为什么不能在C++的`std::map`中存储引用?

60

我理解引用不是指针,而是对象的别名。但是作为程序员,这对我具体意味着什么,即引用在底层是什么,我仍然不太清楚。

我认为最好的方法是理解为什么我不能在映射(map)中存储引用。

我知道我需要停止将引用视为指针的语法糖,只是不确定如何做到 :/


6
不要将引用视为受限制的指针,而应将其视为现有对象的别名,这些对象不允许悬空。 - sbi
1
只需使用std::reference_wrapper - baltermia
6个回答

38

就我所理解的,引用在底层实现时是作为指针来实现的。你不能将它们存储在映射中的原因纯粹是语义上的;必须在创建引用时初始化,并且之后不能再更改它。这与映射的工作方式不符。


1
你认为引用会占用任何“内存”吗?我知道指针会。 - Jaywalker
2
@Jaywalker:是的,它们是。在生成的汇编代码中,它们被实现为指针。 - Sebastiaan M
9
标准文档明确没有说明它们是否具有存储空间。通常情况下,它们会有存储空间,但有时可能会被优化掉。 - sp2danny

36
你应该将引用看作是“指向非常量对象的const指针”:
MyObject& ~~ MyObject * const

此外,引用只能作为现有东西的别名来构建(指针不需要,但除了NULL之外还是建议这样做)。这并不保证对象会一直存在(如果对象不存在,则通过引用访问对象时可能会出现核心转储),请考虑以下代码:
// Falsifying a reference
MyObject& firstProblem = *((MyObject*)0);
firstProblem.do(); // undefined behavior

// Referencing something that exists no more
MyObject* anObject = new MyObject;
MyObject& secondProblem = *anObject;
delete anObject;
secondProblem.do(); // undefined behavior

现在,STL容器有两个要求:
  • T必须是默认可构造的(引用不行)
  • T必须是可赋值的(你不能重置一个引用,但你可以给它的引用者分配值)
因此,在STL容器中,你必须使用代理或指针。
现在,使用指针可能会对内存处理造成问题,因此你可能需要: 请勿使用auto_ptr,因为它会修改右操作数,从而导致问题。
希望这有所帮助 :)

3
小细节:这个 MyObject& firstProblem = *((MyObject*)0); 已经是未定义行为了(你没有使用一个有效对象来初始化引用),因此根据 C++ 标准,它可以格式化你的硬盘或者做其他坏事。(有鼻妖恶魔吗?)但这只是一个非常小的问题,除此之外,你的回答非常好。+1 - sbi
1
另一个小点:在我看来,默认可构造性不是必需的(除了一些方法使用默认构造实例作为默认参数)。 (20.1.4.1)我认为你指的是可复制 - UncleBens
对于STL容器而言,通常来说并不需要确切的(Default Constructible)值。但是对于map而言,如果不使用Default Constructible值,则会使得下标操作符(-> operator[])无效。而且,如果我尝试使用一个非默认构造类型,在VC++2008中会出现错误消息:"error C2512: 'Special::Special' : no appropriate default constructor available"。 - Matthieu M.

9
除了语法糖之外,重要的区别在于引用不能被更改为指向初始化时不同的对象。这就是为什么它们不能存储在映射或其他容器中的原因,因为容器需要能够修改它们包含的元素类型。
作为说明:
A anObject, anotherObject;
A *pointerToA=&anObject;
A &referenceToA=anObject;

// We can change pointerToA so that it points to a different object
pointerToA=&anotherObject;

// But it is not possible to change what referenceToA points to.
// The following code might look as if it does this... but in fact,
// it assigns anotherObject to whatever referenceToA is referring to.
referenceToA=anotherObject;
// Has the same effect as
// anObject=anotherObject;

2
除了“容器需要能够修改它们包含的元素类型”这一点,其他都听起来不错。这让人觉得好像地图需要能够更改它存储的数据类型,而实际上它只需要能够更改“元素”本身即可。 - bambams

8

实际上,您可以在地图中使用引用。我不建议在大项目中这样做,因为它可能会导致奇怪的编译错误,但是:

    map<int, int&> no_prob;
    int refered = 666;
    no_prob.insert(std::pair<int, int&>(0, refered)); // works
    no_prob[5] = 777; //wont compile!!! 
    //builds default for 5 then assings which is a problem
    std::cout << no_prob[0] << std::endl; //still a problem
    std::cout << no_prob.at(0) << std::endl; //works!!

你可以使用map,但很难保证它被正确地使用。然而,我通常在小型代码(通常是竞赛代码)中使用它。


2

存储引用的容器在构建时必须初始化其所有元素,因此不太实用。

struct container
{
   string& s_;           // string reference
};

int main()
{
   string s { "hello" };
   //container {};       // error - object has an uninitialized reference member
   container c { s };    // Ok
   c.s_ = "bye";
   cout << s;            // prints bye
}

另外,一旦初始化完成,容器元素的存储空间就无法更改。s_ 将始终指向上面的 s 的存储空间。


0

1
参考文档中有一些错误。首先,引用并不一定是常量指针(尽管它们可能是这样实现的)。请参见底部jefito的评论。 - Trent

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