何时使用引用与指针

con*_*nec 366 c++ pointers reference

我理解指针与引用的语法和一般语义,但是我应该如何决定何时在API中使用引用或指针?或多或少?

当然,有些情况需要一个或另一个(operator++需要一个引用参数),但总的来说,我发现我更喜欢使用指针(和const指针),因为语法清楚地表明变量是破坏性地传递的.

例如,在以下代码中:

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
}
Run Code Online (Sandbox Code Playgroud)

使用指针,它总是(更加)明显地发生了什么,所以对于API等,清晰度是一个大问题,指针不是比引用更合适?这是否意味着只应在必要时使用引用(例如operator++)?是否有任何性能问题?

编辑(已完成):

除了允许NULL值和处理原始数组之外,似乎选择归结为个人偏好.我已经接受了下面引用Google的C++样式指南的答案,因为它们提出了"引用可能令人困惑,因为它们具有值语法但指针语义"的观点.

由于清理不应该为NULL的指针参数所需的额外工作(例如,add_one(0)将调用指针版本并在运行时中断),从可维护性的角度来看,使用必须存在对象的引用是有意义的,尽管这是一种耻辱失去句法的清晰度.

Kla*_*aim 285

尽可能使用参考,指向任何地方的指针.

避免指针,直到你不能.

原因是指针比其他任何结构都更难以跟踪/阅读,安全性更低,操作更危险.

所以经验法则是只有在没有其他选择时才使用指针.

例如,当函数在某些情况下可以返回nullptr并且假设它将返回时,返回指向对象的指针是一个有效选项.也就是说,更好的选择是使用类似的东西boost::optional.

另一个例子是使用指向原始内存的指针来进行特定的内存操作.这应隐藏并本地化在代码的非常狭窄的部分,以帮助限制整个代码库的危险部分.

在您的示例中,使用指针作为参数没有意义,因为:

  1. 如果你提供nullptr作为参数,你将进入undefined-behavior-land;
  2. 引用属性版本不允许(不容易发现技巧)1的问题.
  3. 对于用户来说,引用属性版本更容易理解:您必须提供有效的对象,而不是可以为null的对象.

如果函数的行为必须使用或不使用给定对象,那么使用指针作为属性表明您可以nullptr作为参数传递,它对于函数来说很好.这是用户和实现之间的契约.

  • 我不确定指针会让你更难读懂吗?这是一个相当简单的概念,并且可以清楚地表明某些内容可能会被修改.如果没有任何迹象表明发生了什么,我会说它更难以阅读,为什么`add_one(a)`不返回结果,而不是通过引用设置它? (48认同)
  • @connec:如果`add_one(a)`令人困惑,那就是因为它的名字不正确.`add_one(&a)`会有同样的混淆,只是现在你可能会增加指针而不是对象.`add_one_inplace(a)`会避免一切混乱. (46认同)
  • 有一点,引用可以指代可以像指针一样容易消失的内存.所以它们不一定比指针更安全.持久化和传递引用可能与指针一样危险. (20认同)
  • @Klaim我的意思是原始指针.我的意思是C++有指针,`NULL`和`nullptr`,并且它有它们是有原因的.并且,给予"永不使用指针"和/或"永不使用NULL,始终使用`boost :: optional`"并不是一个考虑周全甚至是现实的建议.那只是疯了.不要误解我的意思,C++中的原始指针需要的次数少于C,但它们仍然很有用,它们并不像某些C++人想要的那样"危险"(这也是夸大其词),而且:什么时候更容易使用指针和`return nullptr;`表示缺少值...为什么导入整个Boost? (6认同)
  • @Klaim"使用NULL是不好的做法" - 现在这太荒谬了.并且`if`已经过时了,应该使用`while(){break; 相反,对吧?另外,不用担心,我已经看过并使用过大型代码库,是的,*如果你不小心*,那么所有权就是一个问题.但是,如果您坚持使用约定,请始终如一地使用它们并评论和记录您的代码.但毕竟我应该使用C,因为我对C++太愚蠢,对吧? (5认同)
  • @NicolBolas hmmmm`add_one_inplace(a)`看起来不是很好IMO.如果你有更多的参数怎么办?`add_and_check_inplace_inplace_notinplace_notinplace(a,b,c,d)`? (4认同)
  • connec>简单的构造,单独易于理解,但在上下文中难以理解.当你在C++中使用指针时,如果你没有添加大量的断言,你几乎不能保证指向什么. (2认同)
  • @ user805547实际上,目前建议使用指针作为"不拥有"指针,因为它比其他替代品便宜.更明确的标准指针类型对于使其更清晰是有用的,但现在可以使用原始指针来表示缺少所有权,至少在现代c ++代码库中是这样. (2认同)
  • 嗯......导入一个完整的数百兆字节的库只是为了在我们已经拥有一个“可选”对象时拥有一个“可选”对象......我认为这有点夸张。指针也有它们的用途。 (2认同)
  • @ H2CO3我认为你误解了我的陈述:我的意思是不建议使用NULL,因为现在我们有了nullptr.它们基本上做同样的事情(设置指向null的指针也不错......)但是在可以使用int的地方可以使用NULL,因此通常会出现问题:http://thenewcpp.wordpress.com/ 2011/11/03/nullptr /另外,不要推断.约定和注释很好,但编译器不会检查它们.错误是人类,假设每个人都不会粗心大意是完全错误的.不,你不是傻瓜,但你似乎不知道我的观点背后的论点. (2认同)
  • @ H2CO3其实没有,这些粗心的人是你我,任何人,因为没有人不会犯错误. (2认同)

And*_*gia 59

性能完全相同,因为引用在内部实现为指针.因此,您无需担心这一点.

关于何时使用引用和指针,没有普遍接受的约定.在少数情况下,您必须返回或接受引用(例如,复制构造函数),但除此之外,您可以根据需要自由地执行.我遇到的一个相当常见的约定是当参数必须在NULL值为ok时引用现有对象和指针时使用引用.

一些编码约定(如谷歌的)规定应该总是使用指针或const引用,因为引用有一些不清楚的语法:它们具有引用行为但值语法.

  • @Dan:Google风格指南适用于Google的旧代码,不应用于现代编码.事实上,对于一个新项目来说,它是一种相当糟糕的*编码风格. (44认同)
  • @connec:让我这样说吧:null是*完全有效的*指针值.在任何地方都有一个指针,我可以给它值null.你的第二个版本`add_one`是*broken*:`add_one(0); //传递一个完全有效的指针值`,kaboom.你需要检查它是否为空.有些人会反驳说:"好吧,我只记录我的函数不能用null".那很好,但是你打败了问题的目的:如果你要查看文档以查看null是否正常,*你还会看到函数声明*. (13认同)
  • 您好,我在评论中看到缺少语言律师,所以让我补救一下:引用通常是*通过指针实现,但标准说没有这样的事情.使用其他一些机制的实施将是100%投诉. (12认同)
  • 只是为了补充一点,Google的[样式指南说](http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Reference_Arguments)输入函数的参数应该是const引用,输出应该是指针.我喜欢这个,因为当你读取一个函数签名时,它是非常清楚什么是输入和什么是输出. (10认同)
  • 如果它是一个参考,你会看到情况.但是这样的反驳忽略了这一点:引用强制*在语言级别*它引用现有对象,并且不可能为null,而指针没有这样的限制.我认为很明显,语言级执行比文档级执行更强大,更不容易出错.有些人会试着通过这样说来反驳:"看,空引用:`int&i =*((int*)0);`.这不是一个有效的反驳.*前面代码中的问题在于指针使用,而不是引用*.引用永远不会为null,句点. (8认同)
  • 我不同意没有公认的惯例.始终使用引用,除非您需要传达一个对象的不存在,然后传递一个空的智能指针.你实际上从来没有在C++中传递**指针.有些人可能认为theat数组是通过指针传递的.我再次不同意并且在C++中说你应该传递对标准容器(或更优选两个迭代器)的引用.只有C通过指针传递数组. (5认同)
  • @connec:如果您认为那时您正在与不了解C++的人交谈.它是一种复杂的语言.但其核心的美妙之处在于使用的简单性.问题在于,来自其他语言的程序员带来了对C++应该像他们的旧语言(C/Java开发人员想到的那样)一样工作的期望,并且当它没有时就会陷入困境. (5认同)
  • @connec:"虽然不得不牺牲句法清晰度,但这是一种耻辱:(`:这是一个相当个人的观点.通过写'foo(&bar)',你只是在传递......一个地址.没有更多.你自己的个人惯例说你的'foo`会修改`bar`,但就我而言,阅读那段代码,我只是困惑,并担心你对对象的地址做了什么(指针算术?删除它?).你不能指望在不知道它的参数是什么的情况下使用函数.如果你知道函数在做什么,那么你不需要`&`的东西. (2认同)
  • @connec:现在,C#有一个`ref`和`out`参数限定符.在某些方面,它更清楚函数将对参数做什么(如果您通过`ref`传递对象引用,信息意味着什么).编写C#代码几个月之后,`ref`和`out`是我在C#中可以不用的语法糖,并且不喜欢在C++中使用.在另一端,我仍然非常想念`friend`和`const`,没有它们,我的C#代码就不那么安全了.你确定你关注这个重要问题吗? (2认同)
  • @paercebal它们实际上与C++引用完全不同.没有`ref`的值类型参数类似于C++中的`MyClass`.没有**`ref`的引用类型参数**类似于`MyClass&`,一个值类型参数,`ref`也映射到`MyClass&`.带有`ref`的引用类型参数最接近于'MyClass*&`.当然有细微差别.然而,最重要的是,在大多数情况下,当处理`class`es时,你将通过引用传递,而不将其标记为`ref`.类似于在C++中传递引用的方式. (2认同)
  • @paercebal你完全忽略了我的观点。在调用站点,按引用传递具有与 C++ 中相同的语法,并且不需要任何语法符号。在调用站点的 C++ 中,您还需要特殊语法来传递 C# 中的“ref”参数。处理引用类型时。唯一不同的是通过引用传递值类型时。您陈述了两件事“如果您通过 ref 传递对象引用,则该信息没有多大意义”,并且“`ref` 和 `out` 是语法”。两者都是完全错误的。我只是希望您不要误导阅读本文的人。 (2认同)

Mah*_*esh 32

来自C++ FAQ Lite -

尽可能使用引用,并在必要时使用指针.

只要您不需要"重新安装",引用通常优先于指针.这通常意味着引用在类的公共接口中最有用.引用通常出现在对象的皮肤上,而指针则出现在内部.

上面的例外是函数的参数或返回值需要"sentinel"引用 - 一个不引用对象的引用.这通常最好通过返回/获取指针,并赋予NULL指针这一特殊意义(引用必须始终为别名对象,而不是取消引用的NULL指针).

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

  • @connec:当然,C 程序员的语言是正确的。但不要错误地将 C++ 视为 C。它是一种完全不同的语言。如果您将 C++ 视为 C,您最终会编写等同于“C with class”的内容(这不是 C++)。 (2认同)

Cal*_*ius 15

我的经验法则是:

  • 使用指针输出或输入/输出参数.因此可以看出价值将会发生变化.(你必须使用&)
  • 如果NULL参数是可接受的值,则使用指针.(确保它const是传入的参数)
  • 如果传入参数不能为NULL且不是基本类型(const T&),则使用对传入参数的引用.
  • 返回新创建的对象时使用指针或智能指针.
  • 使用指针或智能指针作为结构或类成员而不是引用.
  • 使用别名参考(例如int &current = someArray[i])

无论您使用哪一个,如果它们不明显,请不要忘记记录您的功能及其参数的含义.


bar*_*gol 13

免责声明:除了引用不能为NULL或"反弹"(意味着不能改变它们的别名的对象)这一事实,它真的归结为品味问题,所以我不会说"这个更好".

也就是说,我不同意你在帖子中的最后一句话,因为我不认为代码在参考文献中失去了清晰度.在你的例子中,

add_one(&a);
Run Code Online (Sandbox Code Playgroud)

可能比清楚

add_one(a);
Run Code Online (Sandbox Code Playgroud)

因为你知道a的价值很可能会发生变化.另一方面,功能的签名

void add_one(int* const n);
Run Code Online (Sandbox Code Playgroud)

有点不清楚:是n将是一个整数还是一个数组?有时您只能访问(文档记录不清晰)标题和签名

foo(int* const a, int b);
Run Code Online (Sandbox Code Playgroud)

乍一看并不容易解释.

Imho,当没有(重新)分配和重新绑定(在之前解释的意义上)需要时,引用和指针一样好.此外,如果开发人员仅使用指针作为数组,则函数签名不那么模糊.更不用说运算符语法对引用更具可读性这一事实.


pae*_*bal 12

与其他人一样已经回答:一定要用引用,除非该变量的存在NULL/ nullptr真正有效的状态.

约翰卡马克关于这个问题的观点是相似的:

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_ptrstd::shared_ptr可以设计为通过其默认的构造函数"空"指针:

...意味着使用它们而不验证它们不是空的可能会导致崩溃,这正是J. Carmack讨论的全部内容.

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

JonC++问题的回答- 将引用传递给boost :: shared_ptr,以下注释显示即使这样,通过复制或引用传递智能指针并不像人们想的那样明确(我赞成自己" by-reference"默认情况下,但我可能是错的).


小智 7

这不是品味问题.这里有一些明确的规则.

如果要在声明它的范围内引用静态声明的变量,那么使用C++引用,它将是非常安全的.这同样适用于静态声明的智能指针.通过引用传递参数是此用法的示例.

如果你想引用范围比声明范围更宽的范围,那么你应该使用引用计数的智能指针来保证它是完全安全的.

您可以引用集合的元素,并提供语法方便的参​​考,但它并不安全; 该元素可以随时删除.

要安全地保存对集合元素的引用,必须使用引用计数的智能指针.


doc*_*doc 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;
};
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

但随后,临时的事情就会摧毁你的幸福世界。

RefPhone tempPhone(SimCard(666));   //evil temporary
//function referring to destroyed object
tempPhone.getSimId();    //this can happen
Run Code Online (Sandbox Code Playgroud)

因此,如果您盲目地坚持引用,您就会牺牲传递无效指针的可能性来存储对已销毁对象的引用的可能性,这基本上具有相同的效果。

编辑:请注意,我坚持“尽可能使用引用,必须使用指针。除非不能,否则避免使用指针”。来自最受支持和接受的答案(其他答案也表明如此)。虽然这应该是显而易见的,但例子并不表明引用本身是不好的。然而,它们可能会被滥用,就像指针一样,它们可能会给代码带来威胁。


指针和引用之间存在以下差异。

  1. 当谈到传递变量时,按引用传递看起来像按值传递,但具有指针语义(就像指针一样)。
  2. 引用不能直接初始化为0(空)。
  3. Reference(引用,未引用的对象)不可修改(相当于“*const”指针)。
  4. const 引用可以接受临时参数。
  5. 本地常量引用延长临时对象的生命周期

考虑到这些,我目前的规则如下。

  • 使用将在函数范围内本地使用的参数的引用。
  • 当 0(空)是可接受的参数值或者需要存储参数以供进一步使用时,请使用指针。如果 0 (null) 可以接受,我将在参数中添加“_n”后缀,使用受保护的指针(如 Qt 中的 QPointer)或只是记录它。您还可以使用智能指针。与普通指针相比,您必须更加小心地使用共享指针(否则您可能会因设计而导致内存泄漏和责任混乱)。

  • 您的示例的问题不在于引用不安全,而是您依赖对象实例范围之外的某些内容来保持私有成员处于活动状态。`const SimCard & m_card;` 只是一段写得很糟糕的代码。 (3认同)

Dav*_*rtz 5

任何性能差异都会很小,以至于无法使用不太清楚的方法.

首先,一个未提及参考文献通常优越的案例是const参考文献.对于非简单类型,传递const reference避免创建临时值并且不会引起您关注的混淆(因为该值未被修改).在这里,强制一个人传递一个指针会引起你担心的混乱,因为看到所采用的地址并传递给一个函数可能会让你认为这个值发生了变化.

无论如何,我基本上同意你的看法.我不喜欢函数接受引用来修改它们的值,因为这不是函数正在做的事情.在这种情况下,我也更喜欢使用指针.

当您需要返回复杂类型的值时,我倾向于更喜欢引用.例如:

bool GetFooArray(array &foo); // my preference
bool GetFooArray(array *foo); // alternative
Run Code Online (Sandbox Code Playgroud)

这里,函数名称清楚地表明您在数组中获取信息.所以没有混乱.

引用的主要优点是它们总是包含有效值,比指针更清晰,并且支持多态而无需任何额外的语法.如果这些优点都不适用,则没有理由比指针更喜欢引用.


use*_*723 5

从维基复制-

这样做的结果是,在许多实现中,通过引用对具有自动或静态生存期的变量进行操作,尽管在语法上与直接访问它类似,但可能涉及成本高昂的隐藏取消引用操作。引用是 C++ 语法上有争议的特性,因为它们掩盖了标识符的间接级别。也就是说,与指针通常在语法上很突出的 C 代码不同,在一大块 C++ 代码中,如果正在访问的对象被定义为局部变量或全局变量,或者它是否是对的引用(隐式指针),可能不会立即显而易见。一些其他位置,特别是当代码混合引用和指针时。这方面可能会使写得不好的 C++ 代码更难阅读和调试(请参阅别名)。

我 100% 同意这一点,这就是为什么我认为只有在有充分理由时才应该使用参考文献。


sha*_*pal 5

需要记住的要点:

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

  2. 引用更容易使用,const当我们不想改变值而只需要函数中的引用时可以使用引用。

  3. *与a 一起使用的 while 引用使用的指针&

  4. 当需要进行指针算术运算时,使用指针。

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

指针与引用

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);
Run Code Online (Sandbox Code Playgroud)

判断何时使用什么

指针:用于数组、链表、树实现和指针算术。

参考:在函数参数和返回类型中。