C++ 引用 - 不如房屋安全,但它们可以非常优化






3.50/5 (8投票s)
简要介绍 C++ 引用可以有效使用的实际场景,并讨论使用它们的危险性。
介绍
C++ 引用不如房屋安全。它们实际上并不是一种安全功能,而更多是一种优化。在某些方面,C++ 引用甚至比指针更危险,它们无法被测试或重置,在运行时它们将被假定为有效,甚至可能已被优化掉。 这一点被一些编译器强制执行的语法所抵消,这些语法可以防止你在一些更明显的情况下“玩火自焚”,但这只是象征性的努力。乍一看,C++ 引用可能提供一层安全垫,但它很容易被打破——而且不仅仅是被明显恶意或粗心的代码打破。然而,它们是一种出色的优化,只要你知道你可以信任它们,它们就非常方便。
以下是我敢于涉足的领域。这可以被视为一篇教程,但更像是一个人试图澄清自己如何使用,以免疑虑动摇他对自己代码的信心。
什么是 C++ 引用
乍一看,C++ 引用似乎只是 `const` 指针的语法糖衣,但它远不止于此。
指针,即使是 `const` 指针,也有一个值可以被读取和测试(它可能是空的),以及一个存储该值的地址。C++ 引用则没有。它不是指示编译器创建和持有别名指针,而是**直接指示编译器创建别名**。如果编译器可以在编译时完成其中一些工作,或者优化运行时如何完成,它就可以这样做。它不一定要持有别名指针。C++ 语言不提供对 C++ 引用背后的别名指针或其地址的访问,因为它们可能不存在。
总结一下:当你声明一个指针时,你是在告诉编译器创建空间来存储一个指针。当你声明一个 C++ 引用时,你是在告诉编译器**“我的意思是这个”**,**“就是那个”**,帮我搞定!这是一个很大的区别。使用 C++ 引用,你是在声明你的更高级别的意图,编译器会为此进行优化。
C++ 引用不使用 `->` 指针解引用符号是恰当的,因为可能根本没有指针,并且操作写得好像它们作用于对象本身一样也是恰当的,因为这很可能就是运行时会发生的事情——当然,你已经指示编译器尽可能地接近这一点。
在函数参数定义中
多年来,我唯一信任的 C++ 引用用法是在将接受值的函数参数的定义中。
void function(T& const t) //if I don't want t to be changed
//or
void function(T& t) //if I want to allow t to be changed
我知道它们将接收一个在调用作用域内声明的变量。因此,该变量在函数执行期间保证是有效的。不可能以其他方式干净地调用它。与传递指针相比,它具有语法上的便利性,并且与按值传递相比,它具有性能优势;`t` 不会被复制。此外,如果编译器能够做到,它就可以将引用作为内存中的一个实体以及它在函数调用中的传递进行优化。这都是共赢,我想**许多程序员现在已经采纳了这种用法并且对此感到满意**。
作为类成员
我唯一感到完全放心地使用 C++ 引用的、长生命周期的类成员上下文是作为子对象指向其父对象的反向引用。
class CChild
{
CParent& m_Parent;
public:
CChild::CChild(CParent& Parent)
: m_Parent(Parent)
{
}
};
这仅仅是因为我确信子对象将在父对象中由值或单个所有权智能指针表示,这将确保不会有不属于父对象的子对象。这也是编译器很可能完全优化掉 `m_Parent` 作为运行时解引用中间件的一种情况。所以,尽管**这可能是我能看到的 C++ 引用作为类成员的唯一用途**,但**它是一个很好的用途**。
引用集合的元素
使用 C++ 引用来引用集合的元素是**危险的**,请记住,在运行时遇到无效的 C++ 引用**可能比遇到无效指针更糟糕**。这种情况可能会像这样发生:
CAtlArray<T> Array; //create an array
Array.Add(new T); //add an element
T& t=v[0]; //take a reference to it
Array.RemoveAll(); //empty the array
t.do_something(); //use t and crash!
好的——那个很容易看出问题,但如果 `T& t` 存活得更久,就很容易被忽略。
**尽管有这种危险**,我最近还是涉足了这个领域,因为在一个特定的项目中它提供了出色的优化。在这个项目中,我加载了一组预先编写的表。关键是它们是固定长度的,我从不添加或删除任何项目,虽然我可能会修改它们。这是设计的固有属性。同样是设计的固有属性的是需要非常快速地从文件中加载它们,并大量处理它们的内容。
第一个决定是**将表内存储的行**以**值**的形式存储。下一个决定是预先分配数组到已知的固定大小,并为元素提供空的默认构造函数,以便可以进行块分配,而无需执行任何构造代码。初始化它们没有意义,因为从文件中复制的内容将一次性全部覆盖它们。
我一直知道我想获取数组的一个元素并对其进行大量处理,并且我不想每次想引用它时都从数组中解引用它,我绝对不想每次这样做时都进行复制。**所以我需要获取数组元素的别名并对其进行处理。**
作为局部变量
现在我知道,在我处理数组元素时,不会有任何数组元素被删除或移动,尽管我所做的工作可能很复杂,但这一切都发生在一个函数的作用域内。**受到这种保证**,我第一次声明了一个 C++ 引用作为局部变量,如下所示:
{
CElement& Element=ArrayOfElements[i];
Element.DoThis();
Element DoThat();
}
除了美观之外,它还允许编译器进行最大的优化,比
{
CElement* pElement=&(ArrayOfElements[i]);
pElement->DoThis();;
pElement->DoThat();
}
这强制编译器为存储 `pElement` 提供一个特定的位置,其地址和值必须可读。
无论哪种方式,都可以欣慰地知道您正在处理数组元素本身而不是它的副本**……但请注意 C++ 引用的语法**。您只需要**省略 ampersand**,您就写下了
{
CElement Element=ArrayOfElements[i];
Element.DoThis();
Element DoThat();
}
这是完全不同的**——您正在创建一个副本并对其进行处理。这效率低得多,并且更改将丢失。
**渴望避免这个容易犯的错误**,我采取了措施,为我的 `CElement` 类提供了一个虚拟的私有复制构造函数:
class CElement
{
private:
CElement(CElement & Element )
{
}
public:
CElement( )
{
}
};
这可以防止公共复制的产生,因此
CElement Element=ArrayOfElements[i]; //Error, copy constructor declared private
将无法编译。
作为返回类型
在某些情况下,我想通过特殊的访问函数检索对元素的引用,并首次尝试将 C++ 引用声明为返回类型:
CElement& GetElement(int i)
{
CElement& Element=ArrayOfElements[i];
return Element; //returning a C++ reference
}
当然,许多集合都提供这样的函数,它们返回一个引用,但我们更习惯于使用它们来创建副本。
CElement Element=GetElement(i); //Element is your copy to keep as long as you like
它们还提供了直接通过引用进行操作的选择
{
CElement& Element=GetElement(i); //Element is a reference to the element in the array
Element.DoThis();
Element DoThat();
// but don't do anything here that will disturb the layout of the collection
}
但建议**将局部引用声明 `CElement& Element` 的作用域限制为短暂生命周期**,并且**确保在该集合中使用时没有任何东西会移动**。
结束语
这些是我对使用 C++ 引用的温和而谨慎的尝试。我的总体印象是,C++ 引用提供了**更直接的编程**和潜在的**更直接的执行**,但从安全性的角度来看,**C++ 引用不会照顾您**,而是**您必须照顾它们**。我很好奇听到其他人对它们的经验以及他们如何看待它们。