在API设计中,何时应该使用指针而不是引用?

461
我理解指针和引用的语法和一般语义,但是在API中何时更合适使用引用或指针,我应该如何决定呢?
当然,有些情况需要使用其中之一(例如,`operator++`需要一个引用参数),但总体而言,我发现我更喜欢使用指针(和常量指针),因为语法清楚地表明变量是以破坏性方式传递的。
例如,在以下代码中:
void add_one(int& n) { n += 1; }
void add_one(int* const n) { *n += 1; }
int main() {
  int a = 0;
  add_one(a); // Not clear that a may be modified
  add_one(&a); // 'a' is clearly being passed destructively
}

使用指针时,情况往往更加明显,因此在关注清晰度的API等情况下,指针比引用更合适吗?这是否意味着引用只在必要时使用(例如operator++)?其中一种方法是否存在性能问题?

编辑(已过时)

除了允许NULL值和处理原始数组之外,选择似乎归结为个人偏好。我接受了下面的答案,引用了Google的C++风格指南,因为他们认为“引用可能会令人困惑,因为它们具有值语法但指针语义。”

由于需要额外的工作来清理不应为NULL的指针参数(例如add_one(0)将调用指针版本并在运行时中断),从可维护性的角度来看,使用引用是有道理的,尽管失去了语法上的清晰度有些可惜。


5
看起来你已经对何时使用哪个选项做出了决定。个人而言,无论我是否修改它,我更喜欢传递我要操作的对象。如果一个函数接受一个指针,那就告诉我它在处理指针,即在数组中使用它们作为迭代器。 - Benjamin Lindley
1
@Schnommus:好的,我主要使用TextMate。不过,我认为从一眼看上去就能明白意思是更可取的。 - connec
6
add_one(a);这段代码中哪里不清楚表明变量a将被修改了?代码中明确写着:加一 - GManNickG
44
@connec: 谷歌C++风格指南并不被认为是一个好的C++风格指南。它是用于处理谷歌旧的C++代码库的风格指南(即仅适用于他们的东西)。根据这个接受答案并没有帮助任何人。仅从你的评论和解释来看,你对这个问题已经有了一个已设定的观点,只是在寻找其他人来确认你的观点。因此,你基于自己期望听到的内容来提出问题和回答问题。 - Martin York
1
这可以通过将方法命名为 addOneTo(...) 来简单地解决。如果这不是你想要做的事情,只需查看声明即可。 - stefan
显示剩余8条评论
17个回答

380

尽可能使用引用,必要时使用指针。

在能避免使用指针的情况下尽量避免使用。

原因是指针会使代码更难以理解和阅读,比其他结构更不安全,且操纵起来更危险。

所以经验法则是只有在没有其他选择的情况下才使用指针。

例如,当函数在某些情况下可以返回 nullptr 并且可以假定会返回时,将对象的指针作为返回值是一个合理的选项。不过,更好的选择是使用类似于 std::optional 的东西(需要 C++17;在此之前,可以使用 boost::optional)。

另一个例子是在特定内存操作中使用指向原始内存的指针。这应该被隐藏并限制在代码的非常狭窄的部分,以帮助限制整个代码库中危险部分的影响。

在你的例子中,使用指针作为参数是没有意义的,因为:

  1. 如果您将nullptr作为参数提供,则会进入未定义的行为领域;
  2. 引用属性版本不允许(没有易于发现的技巧)出现1中的问题。
  3. 对于用户来说,引用属性版本更容易理解:您必须提供有效的对象,而不是可能为空的东西。

如果函数的行为需要使用给定对象或不使用给定对象,则使用指针作为属性表明可以将nullptr作为参数传递,并且对于函数来说这是可以接受的。这是用户和实现之间的一种契约。


60
我不确定指针会使阅读变得更困难吗?它是一个相当简单的概念,并且可以清楚地表明何时可能修改某些内容。如果有什么问题,我会说当没有任何指示发生什么事情时,阅读起来更加困难,为什么add_one(a)不返回结果,而是通过引用设置呢? - connec
49
如果add_one(a)让人困惑,那是因为它的命名不妥。add_one(&a)会产生同样的混淆,只是现在你可能会递增指针而不是对象本身。add_one_inplace(a)则可以避免所有的混淆。 - Nicol Bolas
28
一个关键点是,引用可以指向与指针类似会被释放的内存。因此,它们不一定比指针更安全。持久化和传递引用可能与指针一样危险。 - Doug T.
9
@Klaim 我的意思是原始指针。我的意思是C++有指针、NULLnullptr,而且有它们是有原因的。给出“永远不要使用指针”和/或“永远不要使用NULL,总是使用boost::optional”这样的建议并不明智,也不现实。那简直是疯狂的。别误会,相比C语言,C++中需要使用原始指针的情况较少,但它们仍然是有用的。它们并不像某些C++程序员声称的那样“危险”(这也是夸大其词),当使用指针和return nullptr;来表示缺失的值更容易时,为什么要引入整个Boost库呢? - user529758
10
@NicolBolas 嗯嗯,我认为 add_one_inplace(a) 看起来不是很好。如果你有更多的参数呢?add_and_check_inplace_inplace_notinplace_notinplace(a, b, c, d) - mip
显示剩余15条评论

79

由于引用(reference)在内部实现上被实现为指针(pointer),所以它们的表现完全相同,因此您无需担心这个问题。

关于何时使用引用和指针,目前并没有普遍接受的约定。在一些情况下,您必须返回或接受引用(比如复制构造函数),但除此之外,您可以按照自己的意愿随意使用。我遇到的一个相当常见的约定是:当参数必须引用现有对象时,使用引用;当NULL值可行时则使用指针。

某些编码规范(比如Google的)建议总是使用指针或const引用,因为引用具有一些不太清晰的语法:它们具有引用的行为,但值的语法。


13
仅仅想要补充一下,谷歌的编码风格指南指出函数的输入参数应该是const引用,而输出则应该是指针。我喜欢这个规定,因为它能够让人们在阅读函数签名的时候非常清楚地看到哪些是输入,哪些是输出。 - Dan
51
@Dan:Google的风格指南适用于旧的Google代码,不应用于现代编程。事实上,对于新项目来说,它是一种相当糟糕的编码风格。 - GManNickG
13
@connec:让我这样说吧:null是一个完全有效的指针值。在任何有指针的地方,我都可以给它赋值null。因此你的第二个版本的add_one是错误的:add_one(0); // 传递一个完全有效的指针值,炸了。你需要检查它是否为null。有些人会反驳:“好吧,我只需记录下我的函数不接受null即可。”那很好,但这样一来你就失去了问题的意义:如果你要查看文档以确定null是否可行,你也会看到函数声明 - GManNickG
9
如果这是参考文献,你会看到这种情况。然而,这样的反驳忽略了重点:参考文献在语言层面上强制引用现有对象,而不是可能为空,而指针没有此限制。我认为明显语言层面的强制执行比文档层面的强制执行更加有效和不容易出错。一些人会试图反驳这一点,说:“看,空引用:int& i = *((int*)0);。”这不是一个有效的反驳。前面的代码问题在于指针的使用,而不是引用本身。引用永远不会为空。 - GManNickG
13
你好,我看到评论区缺少语言律师,所以让我来解决一下:引用通常是通过指针实现的,但标准并没有规定必须这样做。使用其他机制实现引用也是完全合规的。 - Andreas Bonini
显示剩余17条评论

45

来自 C++ FAQ Lite -

可以使用引用时就使用,必须使用指针时才使用。

只有在不需要"重新定位内存"的情况下才通常使用引用。这通常意味着在类的公共接口中最有用。 引用通常出现在对象的外壳上,指针则出现在内部。

以上的例外是函数的参数或返回值需要一个“哨兵”引用——一个不引用任何对象的引用。 这通常最好通过返回/传递指针并将空指针赋予特殊含义来实现(引用必须始终别名对象,而不是解引用的空指针)。

注意:老式的 C 程序员有时不喜欢引用,因为它们提供了调用者代码中不明确的引用语义。然而,在一些 C++ 经验之后,人们很快意识到这是一种信息隐藏,而不是负担。例如,程序员应该使用问题语言而不是机器语言编写代码。


1
我想你可以争辩说,如果你正在使用API,你应该熟悉它的功能,并知道传递的参数是否被修改...这是需要考虑的事情,但我发现自己同意C程序员的观点(尽管我自己很少有C编程经验)。不过我要补充的是,更清晰的语法对程序员和机器都有好处。 - connec
2
@connec:当然,C程序员对他们的语言是正确的。但不要犯把C++当作C的错误。它是完全不同的语言。如果你把C++当作C来处理,你最终会写出被称为“带类的C”的代码(这不是C++)。 - Martin York

26

我的经验法则是:

  • 对于输出或输入/输出参数,请使用指针。这样可以看到该值将要被更改。(必须使用&
  • 如果NULL参数是可接受的值,请使用指针。(如果是输入参数,务必确保它是const
  • 如果参数不能为NULL并且不是原始类型(const T&< /code>),请使用引用作为输入参数。
  • 在返回新创建的对象时,请使用指针或智能指针。
  • 作为结构体或类成员而不是引用,请使用指针或智能指针。
  • 用于别名(例如:int &current = someArray[i]),请使用引用。

无论您使用哪种方式,请不要忘记记录您的函数及其参数的含义,如果它们不明显。


20
免责声明:除了引用不能为NULL或“反弹”(意味着它们不能更改它们是别名的对象)之外,这实际上归结为品味问题,因此我不会说“这个更好”。
话虽如此,我不同意您在帖子中的最后一句话,我认为代码使用引用并不会失去清晰度。以您的示例为例,
add_one(&a);

可能比...更清晰

add_one(a);

由于您知道a的值很可能会发生变化。但另一方面,函数的签名却不会改变。

void add_one(int* const n);

这里的意思不太清楚:n是一个单独的整数还是一个数组?有时候你只能访问到(文档不完善的)头文件和类似的签名。

foo(int* const a, int b);

起初看起来不容易解释。
在我看来,当不需要重新分配或重新绑定(如上所述)时,引用与指针一样好。此外,如果开发人员仅对数组使用指针,则函数签名有些含糊不清。更不用说操作符语法用引用更易读了。

感谢您清晰地演示了两种解决方案的优缺点。我最初是支持指针的,但这确实很有道理。 - Zach Beavon-Collin

14

像其他人已经回答的那样:除非变量为NULL/nullptr是真正有效的状态,否则始终使用引用。

John Carmack在这个问题上的观点是类似的:

NULL指针是C/C++中最大的问题,至少在我们的代码中是如此。单一值的双重使用作为标志和地址会导致大量的致命问题。应该尽可能使用C++引用而不是指针;虽然引用实际上只是一个指针,但它有一个隐含的约定,即不为NULL。当指针转换为引用时执行NULL检查,然后可以忽略此问题。

http://www.altdevblogaday.com/2011/12/24/static-code-analysis/

编辑 2012-03-13

用户Bret Kuhns正确指出:

C++11标准已经最终确定。我认为现在是时候提到,大多数代码应该使用引用、shared_ptr和unique_ptr的组合就可以完美地解决问题了。

确实如此,但即使用智能指针替换原始指针,问题仍然存在。

例如,无论是std::unique_ptr还是std::shared_ptr都可以通过它们的默认构造函数构造为空指针:

这意味着在没有验证它们不为空的情况下使用它们会有崩溃风险,这正是J.Carmack讨论的核心。

然后,我们面临一个有趣的问题:“如何将智能指针作为函数参数传递?”

Jon回答问题C++ - 传递引用到boost::shared_ptr时,以下评论表明即使是通过复制或引用传递智能指针也并不像人们期望的那样清晰(我个人倾向于默认“按引用”传递,但我可能是错的)。


1
C++11标准已经定稿。我认为在这个主题中提到,大多数代码应该使用引用、shared_ptrunique_ptr的组合来完美地完成。这三个部分和const'ness的组合处理了所有权语义和输入/输出参数约定。在C++中,除了处理遗留代码和高度优化的算法之外,几乎没有必要使用原始指针。它们被使用的领域应该尽可能地封装,并将任何原始指针转换为语义上适当的“现代”等效物。 - Bret Kuhns
1
很多时候智能指针不应该被传递,而是应该测试其是否为null,然后通过引用传递其包含的对象。唯一应该传递智能指针的时候是当你要将所有权(unique_ptr)或共享所有权(shared_ptr)转移/分享到另一个对象时。 - Luke Worth
@povman:我完全同意:如果所有权不是接口的一部分(除非它即将被修改),那么我们不应该将智能指针作为参数(或返回值)传递。当所有权是接口的一部分时,情况会变得有些复杂。例如,Sutter/Meyers关于如何传递unique_ptr作为参数的辩论:通过复制(Sutter)还是通过右值引用(Meyers)?反模式依赖于传递指向全局shared_ptr的指针,存在指针无效的风险(解决方案是在堆栈上复制智能指针)。 - paercebal

8
这不是一个品味问题,以下是一些明确的规则。
如果您想在声明变量的范围内引用静态声明的变量,则使用C ++引用,并且它将是完全安全的。同样适用于静态声明的智能指针。按引用传递参数就是这种用法的例子。
如果您想引用超出声明范围的任何内容,则应使用引用计数智能指针才能确保完全安全。
为了语法方便,您可以使用引用来引用集合中的元素,但这并不安全;该元素可以随时被删除。
要安全地持有对集合元素的引用,则必须使用引用计数智能指针。

7

“尽可能使用引用”规则存在问题,如果您想保留引用以供进一步使用,则会出现这种情况。为了举例说明,假设您拥有以下类。

class SimCard
{
    public:
        explicit SimCard(int id):
            m_id(id)
        {
        }

        int getId() const
        {
            return m_id;
        }

    private:
        int m_id;
};

class RefPhone
{
    public:
        explicit RefPhone(const SimCard & card):
            m_card(card)
        {
        }

        int getSimId()
        {
            return m_card.getId();
        }

    private:
        const SimCard & m_card;
};

起初,将RefPhone(const SimCard & card)构造函数中的参数传递为引用似乎是个好主意,因为它可以防止将错误/空指针传递给构造函数。它在某种程度上鼓励在堆栈上分配变量并从RAII中受益。
PtrPhone nullPhone(0);  //this will not happen that easily
SimCard * cardPtr = new SimCard(666);  //evil pointer
delete cardPtr;  //muahaha
PtrPhone uninitPhone(cardPtr);  //this will not happen that easily

但是临时变量会破坏你美好的世界。
RefPhone tempPhone(SimCard(666));   //evil temporary
//function referring to destroyed object
tempPhone.getSimId();    //this can happen

所以,如果你盲目地坚持使用引用,就会牺牲传递无效指针的可能性,而得到存储已销毁对象的引用的可能性,这基本上具有相同的效果。
编辑:请注意,我坚持遵守“尽可能使用引用,在必须使用指针时使用指针。在可以避免使用指针的情况下,应该避免使用指针。”的规则,这是最受赞同和接受的答案(其他答案也建议如此)。虽然这显而易见,但示例并不是要表明引用本身是不好的。它们可能会被误用,就像指针一样,并且它们可能会给代码带来自己的威胁。
指针与引用有以下区别:
  1. 在传递变量时,按引用传递看起来像按值传递,但具有指针语义(类似于指针)。
  2. 不能直接将引用初始化为0(null)。
  3. 引用(引用对象而非引用)无法修改(相当于 "* const" 指针)。
  4. const 引用可接受临时参数。
  5. 局部常量引用延长了临时对象的生命周期
考虑到这些,我的现行规则如下:
  • 对于将在函数作用域内部使用的参数,请使用引用。
  • 如果可以接受参数值为0(null),或需要存储参数以供后续使用,则请使用指针。如果可以接受参数值为0(null),我会向参数添加 "_n" 后缀,使用受保护的指针(如 Qt 中的 QPointer)或进行文档说明。您还可以使用智能指针。使用共享指针比使用普通指针更需要小心(否则可能会导致设计上的内存泄漏和责任混乱)。

3
你的例子存在问题并不在于引用是不安全的,而是你依赖于超出对象实例范围的东西来保持私有成员变量的可用性。const SimCard & m_card;只是一段写得很糟糕的代码。 - plamenko
1
@plamenko,恐怕你没有理解这个例子的目的。无论const SimCard & m_card是否正确取决于上下文。本帖的信息并不是引用不安全(尽管如果一个人努力尝试,它们可以不安全)。信息是你不应该盲目地坚持“尽可能使用引用”的信条。这个例子是“尽可能使用引用”教义的激进使用的结果。这一点应该很清楚。 - mip
有两件事情让我对你的回答感到困扰,因为我认为它可能会误导那些试图了解更多相关问题的人。
  1. 这篇文章是单向的,很容易给人留下引用是不好的印象。你只提供了一个关于如何不使用引用的例子。
  2. 在你的例子中并不清楚哪里出了问题。是的,temporary会被销毁,但那一行并没有错,而是类的实现有问题。
- plamenko
你几乎不应该有像 const SimCard & m_card 这样的成员。如果你想要在处理临时对象时更加高效,可以添加一个 explicit RefPhone(const SimCard&& card) 构造函数。 - plamenko
@plamenko 如果你连基本的理解都做不到,那么你的问题比被我的帖子误导更大。我不知道我怎么能更清楚了。看看第一句话。"尽可能使用引用"的口号存在问题!在我的帖子中,你在哪里找到了引用是坏的声明?在我的帖子末尾,你写了在哪里使用引用,所以你是怎么得出这样的结论的?这不是对问题的直接回答吗? - mip
显示剩余2条评论

5

需要注意的几点:

  1. 指针可以为NULL,但引用不能为NULL

  2. 使用引用更加方便,在函数中如果我们不想改变值,只需使用const引用即可。

  3. 指针用*表示,而引用用&表示。

  4. 需要执行指针算术运算时,请使用指针。

  5. 您可以有一个指向void类型的指针int a=5; void *p = &a;,但不能有对void类型的引用。

指针 vs 引用

void fun(int *a)
{
    cout<<a<<'\n'; // address of a = 0x7fff79f83eac
    cout<<*a<<'\n'; // value at a = 5
    cout<<a+1<<'\n'; // address of a increment by 4 bytes(int) = 0x7fff79f83eb0
    cout<<*(a+1)<<'\n'; // value here is by default = 0
}
void fun(int &a)
{
    cout<<a<<'\n'; // reference of original a passed a = 5
}
int a=5;
fun(&a);
fun(a);

使用指针的情况

指针: 用于数组、链表、树等数据结构的实现以及指针运算。

引用: 用于函数参数和返回类型。


  1. 如果数组的大小是固定的,您不必使用指针来传递它们。
  2. 无论如何最好传递span而不是数组。
  3. 并不总是返回引用是一个好主意。
- einpoklum

5
任何性能差异都非常小,无法证明使用不太明确的方法。
首先,一个未提及到的情况通常更倾向于使用引用,即带有“const”的引用。对于非简单类型,传递一个“const引用”避免创建临时对象并且不会造成您所担心的困惑(因为值不会改变)。在这种情况下,强迫一个人传递指针会导致您所担心的混淆,因为看到取地址并将其传递给函数可能会让您认为值已经改变了。
总之,我基本上同意你的观点。当函数修改值时,如果很难明显表达出来,我也喜欢使用指针。
当需要返回复杂类型中的值时,我倾向于使用引用。例如:
bool GetFooArray(array &foo); // my preference
bool GetFooArray(array *foo); // alternative

这里,函数名称清楚地表明你将得到一个数组中的信息,因此不会产生混淆。

引用的主要优点是它们始终包含有效值,比指针更简洁,并且支持多态性而无需任何额外的语法。如果没有这些优点适用,则没有理由优先选择引用而不是指针。


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