【问题标题】:Is it wrong to dereference a pointer to get a reference?取消引用指针以获取引用是错误的吗?
【发布时间】:2011-03-27 13:07:15
【问题描述】:

我更喜欢在任何地方都使用引用,但是当您使用 STL 容器时,您必须使用指针,除非您真的想要按值传递复杂类型。而且我觉得转换回参考很脏,这似乎是错误的。

是吗?

澄清一下……

MyType *pObj = ...
MyType &obj = *pObj;

这不是“脏”吗,因为您可以(即使只是在理论上,因为您会先检查它)取消引用 NULL 指针?

编辑:哦,你不知道对象是否是动态创建的。

【问题讨论】:

  • 我想说你可以随心所欲地尊重指针,但我不确定这样的行为会意味着什么。
  • @John:您能否告诉我们您存储指针的对象是否是动态分配的?我认为是这种情况,但不是绝对需要。
  • @John:不要让自己处于需要删除任何东西的位置,它需要被包装起来。使用 shared_ptr 实现、指针容器或在 C++0x 中使用 unique_ptr。另外,不要猜测优化。按值存储它们,如果性能成为衡量问题,则存储指向值的智能指针。
  • @Steve:我可以从他的论点中推断出的唯一反对意见是性能是一个问题(因此“真的想按值传递复杂类型”并找到一种“更好的方法”来复制东西。写作hack 错误代码的代码不是好代码,这是一种 hack。应该用损坏的复制语义修复类。

标签: c++ pointers stl reference


【解决方案1】:

我更喜欢在任何地方都使用引用,但是当您使用 STL 容器时,您必须使用指针,除非您真的想按值传递复杂类型。

需要明确一点:STL 容器旨在支持某些语义(“值语义”),例如“容器中的项目可以被复制”。由于引用不可重新绑定,它们不支持值语义(即,尝试创建 std::vector<int&>std::list<double&>)。您是正确的,您不能将引用放在 STL 容器中。

通常,如果您使用引用而不是普通对象,那么您要么使用基类并希望避免切片,要么试图避免复制。而且,是的,这意味着如果您想将项目存储在 STL 容器中,那么您将需要使用指针来避免切片和/或复制。

而且,是的,以下是合法的(尽管在这种情况下,不是很有用):

#include <iostream>
#include <vector>

// note signature, inside this function, i is an int&
// normally I would pass a const reference, but you can't add
// a "const* int" to a "std::vector<int*>"
void add_to_vector(std::vector<int*>& v, int& i)
{
    v.push_back(&i);
}

int main()
{
    int x = 5;
    std::vector<int*> pointers_to_ints;

    // x is passed by reference
    // NOTE:  this line could have simply been "pointers_to_ints.push_back(&x)"
    // I simply wanted to demonstrate (in the body of add_to_vector) that
    // taking the address of a reference returns the address of the object the
    // reference refers to.
    add_to_vector(pointers_to_ints, x);

    // get the pointer to x out of the container
    int* pointer_to_x = pointers_to_ints[0];

    // dereference the pointer and initialize a reference with it
    int& ref_to_x = *pointer_to_x;

    // use the reference to change the original value (in this case, to change x)
    ref_to_x = 42;

    // show that x changed
    std::cout << x << '\n';
}

哦,你不知道对象是否是动态创建的。

这不重要。在上面的示例中,x 在堆栈上,我们将指向x 的指针存储在pointers_to_vectors 中。当然,pointers_to_vectors 在内部使用动态分配的数组(当vector 超出范围时,delete[]s 该数组),但该数组包含指针,而不是指向的东西。当pointers_to_ints 超出范围时,内部int*[]delete[]-ed,但int*s 不是deleted。

事实上,这使得在 STL 容器中使用指针变得困难,因为 STL 容器不会管理指向对象的生命周期。您可能想查看 Boost 的指针容器库。否则,您将 (1) 想要使用智能指针的 STL 容器(例如 boost:shared_ptr,这对于 STL 容器是合法的)或 (2) 以其他方式管理指向对象的生命周期。您可能已经在做 (2)。

【讨论】:

    【解决方案2】:

    这并没有什么问题,但请注意,在机器代码级别上,引用通常与指针相同。因此,通常在分配给引用时,指针并没有真正取消引用(没有内存访问)。 因此,在现实生活中,引用可以为 0,并且在使用引用时会发生崩溃 - 可能会比其分配晚得多。

    当然,发生的情况在很大程度上取决于编译器版本和硬件平台以及编译器选项和引用的确切用法。

    官方取消引用 0 指针的行为是未定义,因此任何事情都可能发生。这任何事情包括它可能会立即崩溃,但也可能会在很久以后崩溃或永远不会崩溃。

    因此,请始终确保永远不要将 0 指针分配给引用 - 很难找到像这样的错误。

    编辑:使“通常”斜体并添加有关官方“未定义”行为的段落。

    【讨论】:

    • 嗯,C++ 标准是否要求引用实现为直接指针?
    • 否,但大多数编译器通常会这样做。因此,在实践中经常取消对 0 指针的引用并将其分配给引用是可能的,并且可能会导致在其他位置发生奇怪的崩溃。我更新了我的答案以更清楚地说明这一点。
    【解决方案3】:

    在尝试将指针转换为引用之前,确保指针不为 NULL,并且只要您的引用存在(或保持已分配,参考堆),对象将保持在范围内,并且您会没事的,道德上干净的:)

    【讨论】:

    • 动态分配的对象如何在作用域内?
    • 空引用(不方便)未定义。
    • @Billy:我们不知道它们是动态分配的,只是我们指向它们。
    • @Billy:重新阅读他们写的内容。他们正在使用 STL 容器,但他们不想将其按值存储在容器中,因此需要在插入时使用复制构造函数。他们想要一个包含(智能)指针的容器,这些指针指向可能是动态分配的,也可能不是(例如静态数组)。
    • 智能指针可以有空析构函数。如果您有一个指向对象的智能指针向量,但出于某种原因想要存储堆栈分配的对象,这是一件好事。
    【解决方案4】:

    使用取消引用的指针初始化引用绝对没问题,没有任何问题。如果p 是一个指针,并且如果取消引用它是有效的(例如它不为空),那么*p 就是它指向的对象。您可以将引用绑定到该对象,就像您将引用绑定到任何对象一样。显然,您必须确保引用不会超过对象(就像任何引用一样)。

    例如,假设我收到了一个指向对象数组的指针。它也可以是一个迭代器对、一个对象向量或一个map 对象,但为了简单起见,我将使用一个数组。每个对象都有一个函数order,返回一个整数。我要在每个对象上调用一次bar 函数,以增加order 的值:

    void bar(Foo &f) {
        // does something
    }
    
    bool by_order(Foo *lhs, Foo *rhs) {
        return lhs->order() < rhs->order();
    }
    
    void call_bar_in_order(Foo *array, int count) {
        std::vector<Foo*> vec(count);  // vector of pointers
        for (int i = 0; i < count; ++i) vec[i] = &(array[i]);
        std::sort(vec.begin(), vec.end(), by_order);
        for (int i = 0; i < count; ++i) bar(*vec[i]); 
    }
    

    我的示例初始化的引用是函数参数,而不是直接变量,但我可以有效地完成:

    for (int i = 0; i < count; ++i) {
        Foo &f = *vec[i];
        bar(f);
    }
    

    显然vector&lt;Foo&gt; 是不正确的,因为那时我会按顺序在每个对象的副本 上调用bar,而不是按顺序在每个对象上调用。 bar 接受一个非常量引用,所以除了性能或其他任何东西之外,如果 bar 修改输入,那显然是错误的。

    智能指针向量或提升指针向量也是错误的,因为我不拥有数组中的对象,当然也不能释放它们。也可能不允许对原始数组进行排序,或者如果它是 map 而不是数组,则不可能。

    【讨论】:

    • 对,有时您只需要一个原始指针,允许在其他地方处理所有权。
    • 如果向量的范围安全地在引用都有效的某个函数内,没有人会受到伤害......
    【解决方案5】:

    我的回答并没有直接解决您最初的担忧,但您似乎遇到了这个问题,因为您有一个存储指针类型的 STL 容器。

    Boost 提供了ptr_container 库来解决这些类型的情况。例如,ptr_vector 在内部存储指向类型的指针,但通过其接口返回引用。请注意,这意味着容器拥有指向实例的指针并将管理其删除。

    这里有一个简单的例子来演示这个概念。

    #include <string>
    #include <boost/ptr_container/ptr_vector.hpp>
    
    void foo()
    {
        boost::ptr_vector<std::string> strings;
    
        strings.push_back(new std::string("hello world!"));
        strings.push_back(new std::string());
    
        const std::string& helloWorld(strings[0]);
        std::string& empty(strings[1]);
    }
    

    【讨论】:

    • 不错。智能指针是有效内置的,但容器提供了引用语义。
    【解决方案6】:

    如果你希望容器真正包含动态分配的对象,你不应该使用原始指针。使用unique_ptr 或任何合适的类似类型。

    【讨论】:

    【解决方案7】:

    没有。你还能如何实现operator=?您必须取消引用 this 才能返回对您自己的引用。

    请注意,尽管我仍会按值将项目存储在 STL 容器中 - 除非您的对象很大,否则堆分配的开销将意味着您使用的存储空间更多,效率更低如果您只是按值存储项目。

    【讨论】:

    • @Billy:关于开销,您可能是对的。话虽如此,偶尔也有无法复制的对象。
    • 开销不取决于您使用的容器类型吗?例如,std::vector 以块的形式保留内存,并且不会为添加到其中的每个元素进行单独的分配。 std::setstd::map 实现可以很好地为每个元素执行单独的分配,从而产生你所说的开销。或者也许还有其他我没有考虑的事情。请详细说明。
    • @A. Levy:vector 将根据需要重新分配,将实例从旧缓冲区复制到新缓冲区。但是,是的,它会分配一个连续的范围并使用位置new 在这些位置实例化副本。地图可能需要每个节点一个块,但话又说回来,它不太可能复制一个节点。
    • @Steven:除非你复制地图本身。
    • 这也意味着你的班级必须有一个空/默认的ctor。这可能意味着编写额外的代码只是为了允许您在此状态下的对象无效时将它们放入容器中。只是看起来很乱。
    猜你喜欢
    • 1970-01-01
    • 2018-12-22
    • 1970-01-01
    • 1970-01-01
    • 2021-07-25
    • 1970-01-01
    • 1970-01-01
    • 2011-02-13
    相关资源
    最近更新 更多