什么时候使用引用和指针

我了解指针和引用的语法和一般语义,但是我应该如何决定什么时候在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
}

使用指针,它总是(更)明显的发生了什么,所以对于api和类似的地方,清晰度是一个大问题,指针不是比引用更合适吗?这是否意味着引用应该只在必要时使用(例如operator++)?其中一种是否存在性能问题?

编辑(过时的):

除了允许NULL值和处理原始数组之外,选择似乎还取决于个人喜好。我已经接受了下面的答案,引用谷歌的c++风格指南,因为它们提出了“引用可能令人困惑,因为它们有值语法但指针语义。”

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

229845 次浏览

尽可能使用引用,必须使用指针。

避免指针,直到你不能。

原因是指针使事情更难跟踪/阅读,比任何其他结构更不安全,更危险的操作。

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

例如,当函数在某些情况下可以返回nullptr并且假定它会返回时,返回指向对象的指针是一个有效的选项。也就是说,更好的选择是使用类似std::optional的东西(需要c++ 17;在那之前,有boost::optional)。

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

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

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

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

两者的性能完全相同,因为引用是作为指针在内部实现的。因此你不需要担心这个。

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

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

任何性能差异都是如此之小,以至于使用不太清楚的方法都是不合理的。

首先,一种没有提到的引用通常更好的情况是const引用。对于非简单类型,传递const reference可以避免创建一个临时类型,也不会引起你所关心的混乱(因为该值没有被修改)。在这里,强制某人传递指针会导致您所担心的混乱,因为看到地址被获取并传递给函数可能会使您认为值已更改。

无论如何,我基本上同意你的看法。我不喜欢函数接受引用来修改它们的值,而这不是很明显的函数在做什么。在这种情况下,我也更喜欢使用指针。

当需要以复杂类型返回值时,我倾向于使用引用。例如:

bool GetFooArray(array &foo); // my preference
bool GetFooArray(array *foo); // alternative

在这里,函数名清楚地表明您正在从数组中获取信息。这样就不会混淆了。

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

c++ FAQ Lite -

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

当你不需要引用时,引用通常比指针更受欢迎 “修整”。这通常意味着引用在类中最有用 类的公共接口。的皮肤上通常出现引用

上述情况的例外是函数的形参或返回值 值需要一个“哨兵”引用——一个不引用的引用 到一个对象。这通常通过返回/获取一个指针来实现, 并赋予NULL指针这个特殊的意义(引用必须 总是别名对象,而不是解引用的空指针) 注意:老行C程序员有时不喜欢引用,因为 它们提供的引用语义在调用者的引用语义中并不显式 代码。然而,经过一些c++经验之后,人们很快就会意识到这是错误的 一种信息隐藏形式,是一种资产,而不是 责任。例如,程序员应该用编程语言编写代码 问题而不是机器的语言。

维基-复制

这样做的结果是,在许多实现中,通过引用操作具有自动或静态生命周期的变量,尽管在语法上类似于直接访问它,但可能涉及代价高昂的隐藏的解引用操作。引用是c++中一个在语法上有争议的特性,因为它们模糊了标识符的间接级别;也就是说,不像在C代码中,指针通常在语法上突出,在一大块c++代码中,如果被访问的对象被定义为局部变量或全局变量,或者它是否是指向其他位置的引用(隐式指针),可能不会立即明显,特别是当代码混合了引用和指针时。这个方面会使编写得很差的c++代码更难阅读和调试(参见别名)。

我完全同意这一点,这就是为什么我认为只有在你有充分理由的时候才应该使用推荐信。

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

约翰·卡马克在这个问题上的观点也类似:

空指针是C/ c++中最大的问题,至少在我们的代码中是这样。将一个值同时用作标志和地址会导致大量致命问题。在任何可能的情况下,c++引用应该比指针更受欢迎;虽然引用“实际上”只是一个指针,但它有一个非null的隐含契约。当指针转换为引用时执行NULL检查,然后可以忽略此问题。

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

编辑2012-03-13

用户布雷特·库恩正确地指出:

c++ 11标准已经完成。我认为是时候在这个线程中提到,大多数代码应该可以很好地使用引用、shared_ptr和unique_ptr的组合。

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

例如,std::unique_ptrstd::shared_ptr都可以构造为“empty”;指针通过默认构造函数:

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

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

乔恩回答用于问题c++ -将引用传递给boost::shared_ptr,并且下面的注释表明,即使这样,通过复制或引用传递智能指针也不像人们希望的那样明确(我喜欢使用&;by-reference"默认,但我可能错了)。

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

也就是说,我不同意你在文章中最后的说法,因为我不认为代码在引用中失去了清晰度。在你的例子中,

add_one(&a);

可能会更清楚

add_one(a);

因为你知道a的值很可能会改变。另一方面,函数的签名

void add_one(int* const n);

也不太清楚n是单个整数还是数组?有时您只能访问(文档记录不佳的)头文件和签名

foo(int* const a, int b);

乍一看不容易理解。

在我看来,当不需要(重新)分配或重新绑定(在前面解释的意义上)时,引用和指针一样好。此外,如果开发人员只对数组使用指针,那么函数签名就不会那么模糊。更不用说使用引用的操作符语法可读性更强了。

我的经验法则是:

  • 使用指针作为输出或in/out参数。所以可以看出,这个值是会改变的。(你必须使用&)
  • 如果NULL参数是可接受的值,则使用指针。(如果是传入参数,则确保为const)
  • 如果传入参数不能为NULL且不是基本类型(const T&),则使用引用。
  • 在返回新创建的对象时使用指针或智能指针。
  • 使用指针或智能指针作为结构或类成员,而不是引用。
  • 使用引用来别名(例如。int &current = someArray[i])

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

对于指针,您需要它们指向某个对象,因此指针需要占用内存空间。

例如,接受整数指针的函数将不接受整数变量。因此,您需要首先为它创建一个指针,以便传递给函数。

作为参考,它不会占用内存。你有一个整数变量,你可以把它作为引用变量传递。就是这样。您不需要专门为它创建引用变量。

只是把我的一角硬币放进去。我刚做了个测试。一个打喷嚏的人。我只是让g++使用指针而不是引用来创建同一个小程序的程序集文件。 当查看输出时,它们是完全相同的。除了符号命名。所以看一下性能(在一个简单的例子中),没有问题

现在谈谈指针与引用的话题。恕我直言,我认为清晰是最重要的。一旦我读到内隐行为,我的脚趾就开始卷曲。我同意引用不能为NULL是很好的隐式行为。

解引用NULL指针不是问题所在。它将使您的应用程序崩溃,并且易于调试。一个更大的问题是未初始化的指针包含无效值。这很可能导致内存损坏,导致没有明确起源的未定义行为。

这就是我认为引用比指针更安全的地方。我同意前面的说法,接口(应该清楚地记录,参见合同设计,Bertrand Meyer)定义了函数参数的结果。考虑到这些因素,我的偏好是

.使用引用

这不是品味的问题。以下是一些明确的规则。

如果你想在声明变量的范围内引用静态声明的变量,那么使用c++引用,这将是完全安全的。这同样适用于静态声明的智能指针。通过引用传递参数就是一个例子。

如果你想从一个比它声明的范围更宽的范围中引用任何东西,那么你应该使用一个引用计数智能指针,以确保它的完全安全。

为了语法方便,您可以使用引用引用集合的元素,但这并不安全;元素可以在任何时候被删除。

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

尽可能使用参考资料”规则有问题,如果你想保留引用以供进一步使用,就会出现这个问题。为了举例说明这一点,假设您有以下类。

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

因此,如果你盲目地坚持引用,你就在传递无效指针的可能性与存储已销毁对象引用的可能性之间进行了权衡,这基本上是相同的效果。

edit:请注意,我坚持了这条规则:“尽可能使用引用,必须使用指针。”避免指针,直到你做不到为止。”这是被点赞最多、接受度最高的答案(其他答案也这么认为)。虽然这应该是显而易见的,但例子并不表明引用是不好的。然而,它们也可能被滥用,就像指针一样,它们也会给代码带来威胁。


指针和引用之间有以下区别。

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

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

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

以下是一些指导方针。

函数使用传递的数据而不修改它:

  1. 如果数据对象很小,例如内置数据类型或小结构,则按值传递。

  2. 如果数据对象是数组,则使用指针,因为这是唯一的选择。将指针设置为指向const的指针。

  3. 如果数据对象是一个大小良好的结构,则使用const指针或const 参考提高程序效率。你节省了所需的时间和空间 复制结构或类设计。将指针或引用设为const

  4. 如果数据对象是类对象,则使用const引用。类设计的语义通常需要使用引用,这是c++添加的主要原因 这个特性。因此,传递类对象参数的标准方法是引用

函数修改调用函数中的数据:

< p > 1。如果数据对象是内置数据类型,则使用指针。如果你是现货代码 与fixit(&x)一样,其中x是int型,很明显这个函数打算修改x。

2.如果数据对象是一个数组,则使用唯一的选择:指针。

3.如果数据对象是结构,请使用引用或指针。

4.如果数据对象是类对象,则使用引用。

当然,这些只是指导方针,可能有不同的理由 选择。例如,cin使用基本类型的引用,因此您可以使用cin >>n 而不是cin >>和n。< / p >
引用更干净,更容易使用,而且它们在隐藏信息方面做得更好。 但是,不能重新分配引用。 如果需要先指向一个对象,然后再指向另一个对象,则必须使用指针。引用不能为空,因此如果存在问题对象可能为空的任何可能性,则不能使用引用。你必须使用指针。 如果你想自己处理对象操作,即如果你想为堆上的对象分配内存空间,而不是在堆栈上,你必须使用Pointer

int *pInt = new int; // allocates *pInt on the Heap

要记住的要点:

  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);

判断什么时候用什么

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

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

正确编写的示例应该如下所示

void add_one(int& n) { n += 1; }
void add_one(int* const n)
{
if (n)
*n += 1;
}
这就是为什么在可能的情况下推荐引用 …< / p >

在我的实践中,我个人确定了一个简单的规则——对可复制/可移动的原语和值使用引用,对具有长生命周期的对象使用指针。

对于Node的例子,我肯定会使用

AddChild(Node* pNode);