C ++中指针变量和引用变量之间有什么区别?

时间:2008-09-11 20:03:57

标签: c++ pointers reference c++-faq

我知道引用是语法糖,所以代码更容易读写。

但有什么区别?


以下答案和链接摘要:

  1. 指针可以重新分配任意次数,而在绑定后无法重新分配引用。
  2. 指针可以指向任何地方(NULL),而引用始终指向一个对象。
  3. 您无法使用指针获取引用的地址。
  4. 没有“参考算术”(但您可以获取引用指向的对象的地址,并在&obj + 5上)对其执行指针算法。
  5. 澄清一种误解:

      

    C ++标准非常谨慎,以避免规定编译器的方式   实现引用,但每个C ++编译器都实现   引用作为指针。也就是说,声明如下:

    int &ri = i;
    
         

    如果没有完全优化 会分配相同数量的存储空间   作为指针,并放置地址   将i放入该存储空间。

    因此,指针和引用都使用相同数量的内存。

    作为一般规则,

    • 使用函数参数和返回类型中的引用来提供有用的自我文档接口。
    • 使用指针实现算法和数据结构。

    有趣的阅读:

42 个答案:

答案 0 :(得分:1533)

  1. 可以重新指定指针:

    int x = 5;
    int y = 6;
    int *p;
    p =  &x;
    p = &y;
    *p = 10;
    assert(x == 5);
    assert(y == 10);
    

    引用不能,必须在初始化时分配:

    int x = 5;
    int y = 6;
    int &r = x;
    
  2. 指针在堆栈上有自己的内存地址和大小(x86上为4个字节),而引用共享相同的内存地址(使用原始变量),但也会占用堆栈上的一些空间。由于引用与原始变量本身具有相同的地址,因此可以将引用视为同一变量的另一个名称。注意:指针指向的内容可以在堆栈或堆上。同上参考。我在这个陈述中的主张并不是指针必须指向堆栈。指针只是一个保存内存地址的变量。此变量位于堆栈上。由于引用在堆栈上有自己的空间,并且因为地址与它引用的变量相同。有关stack vs heap的更多信息。这意味着编译器不会告诉您有一个引用的实际地址。

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
    
  3. 您可以指向指向提供额外级别间接的指针的指针。而引用仅提供一个间接层。

    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    pp = &q;//*pp = q
    **pp = 4;
    assert(y == 4);
    assert(x == 0);
    
  4. 指针可以直接分配nullptr,而引用则不能。如果你努力尝试,并且知道如何,你可以建立参考地址nullptr。同样,如果你努力尝试,你可以引用一个指针,然后该引用可以包含nullptr

    int *p = nullptr;
    int &r = nullptr; <--- compiling error
    int &r = *p;  <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
    
  5. 指针可以遍历数组,您可以使用++转到指针指向的下一个项目,+ 4转到第5个元素。这与指针指向的对象无关。

  6. 需要使用*取消引用指针以访问它指向的内存位置,而可以直接使用引用。指向类/结构的指针使用->来访问它的成员,而引用使用.

  7. 指针是一个保存内存地址的变量。无论引用如何实现,引用都具有与其引用的项相同的内存地址。

  8. 引用不能填入数组,而指针可以(由用户@litb提及)

  9. Const引用可以绑定到临时对象。指针不能(不是没有一些间接):

    const int &x = int(12); //legal C++
    int *y = &int(12); //illegal to dereference a temporary.
    

    这使得const&更安全,可以在参数列表等中使用。

答案 1 :(得分:343)

什么是C ++参考( for C programmers

引用可以被认为是一个常量指针(不要与指向常量值的指针混淆!)和自动间接,即编译器将应用适合您的*运算符。

必须使用非null值初始化所有引用,否则编译将失败。获取引用的地址既不可能 - 地址运算符将返回引用值的地址 - 也不可能在引用上进行算术运算。

C程序员可能不喜欢C ++引用,因为当间接发生时,或者通过值或指针传递参数而不查看函数签名时,它将不再是显而易见的。

C ++程序员可能不喜欢使用指针,因为它们被认为是不安全的 - 虽然引用并不比常量指针更安全,除非在最琐碎的情况下 - 缺乏自动间接的便利性并带有不同的语义内涵。

考虑C++ FAQ中的以下陈述:

  

即使引用通常是使用地址中的地址实现的   基础汇编语言,请将引用视为一个   有趣的指向一个对象的指针。引用对象。它是   不是指向对象的指针,也不是对象的副本。它   对象

但如果引用确实是对象,那么怎么会有悬空引用呢?在非托管语言中,引用不可能比指针更“安全” - 通常只是不能跨范围边界可靠地对值进行别名!

为什么我认为C ++引用很有用

来自C背景,C ++引用可能看起来像一个有点愚蠢的概念,但是在可能的情况下仍然应该使用它们而不是指针:自动间接 方便,并且引用在处理时变得特别有用与RAII - 但不是因为任何明显的安全优势,而是因为它们使写作惯用代码不那么尴尬。

RAII是C ++的核心概念之一,但它与复制语义非常简单地交互。通过引用传递对象避免了这些问题,因为不涉及复制。如果语言中没有引用,则必须使用指针,这些指针使用起来比较麻烦,因此违反了语言设计原则,即最佳实践解决方案应该比替代方案更容易。

答案 2 :(得分:170)

如果你想变得非常迂腐,你可以用引号做一件事,你不能用指针做:延长临时对象的生命周期。在C ++中,如果将const引用绑定到临时对象,则该对象的生命周期将成为引用的生命周期。

std::string s1 = "123";
std::string s2 = "456";

std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;

在此示例中,s3_copy复制作为串联结果的临时对象。而s3_reference本质上成为临时对象。它实际上是对临时对象的引用,该临时对象现在具有与引用相同的生命周期。

如果您在没有const的情况下尝试此操作,则无法编译。您不能将非const引用绑定到临时对象,也不能为此处获取其地址。

答案 3 :(得分:115)

与流行的观点相反,可能有一个NULL的引用。

int * p = NULL;
int & r = *p;
r = 1;  // crash! (if you're lucky)

当然,使用参考文件要困难得多 - 但是如果你管理它,你会撕掉你的头发试图找到它。在C ++中,引用本质上是安全的!

从技术上讲,这是无效引用,而不是空引用。 C ++不支持空引用作为您可能在其他语言中找到的概念。还有其他类型的无效引用。 任何无效引用都会引发未定义行为的幽灵,就像使用无效指针一样。

实际错误是在分配给引用之前解除引用NULL指针。但是我不知道任何编译器会在这种情况下产生任何错误 - 错误会传播到代码中的某个点。这就是让这个问题如此阴险的原因。大多数情况下,如果你取消引用一个NULL指针,你就会在那个位置崩溃,并且不需要太多的调试来解决它。

我上面的例子简短且做作。这是一个更现实的例子。

class MyClass
{
    ...
    virtual void DoSomething(int,int,int,int,int);
};

void Foo(const MyClass & bar)
{
    ...
    bar.DoSomething(i1,i2,i3,i4,i5);  // crash occurs here due to memory access violation - obvious why?
}

MyClass * GetInstance()
{
    if (somecondition)
        return NULL;
    ...
}

MyClass * p = GetInstance();
Foo(*p);

我想重申,获取空引用的唯一方法是通过格式错误的代码,一旦拥有它,就会得到未定义的行为。 从不检查空引用是否有意义;例如,您可以尝试if(&bar==NULL)...,但编译器可能会优化该语句!有效引用永远不能为NULL,因此从编译器的视图来看,比较总是为假,并且可以将if子句作为死代码自由消除 - 这是未定义行为的本质。

避免麻烦的正确方法是避免取消引用NULL指针来创建引用。这是实现这一目标的自动方式。

template<typename T>
T& deref(T* p)
{
    if (p == NULL)
        throw std::invalid_argument(std::string("NULL reference"));
    return *p;
}

MyClass * p = GetInstance();
Foo(deref(p));

对于那些有较好写作技巧的人来看这个问题,请参阅Jim Hyslop和Herb Sutter的Null References

关于解除引用空指针的危险的另一个例子,请参阅Raymond Chen的Exposing undefined behavior when trying to port code to another platform

答案 4 :(得分:113)

除了语法糖之外,引用是const指针(不是指向const的指针)。您必须在声明引用变量时确定它所引用的内容,并且以后不能更改它。

更新:现在我再考虑一下,有一个重要的区别。

可以通过获取其地址并使用const转换来替换const指针的目标。

参考目标不能以任何方式替换UB。

这应该允许编译器对引用进行更多优化。

答案 5 :(得分:106)

你忘记了最重要的部分:

使用指针进行成员访问时使用->
使用引用的成员访问使用.

foo.bar 明显优于foo->bar,与vi 明显优于Emacs的方式相同: - )

答案 6 :(得分:63)

引用与指针非常相似,但它们是专门为优化编译器而设计的。

  • 引用的设计使编译器更容易跟踪哪个引用别名哪个变量。两个主要特征非常重要:没有“参考算术”,也没有重新分配参考文献。这些允许编译器在编译时找出哪些引用别名是哪些变量。
  • 允许引用引用没有内存地址的变量,例如编译器选择放入寄存器的变量。如果你取一个局部变量的地址,编译器就很难把它放在一个寄存器中。

举个例子:

void maybeModify(int& x); // may modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // This function is designed to do something particularly troublesome
    // for optimizers. It will constantly call maybeModify on array[0] while
    // adding array[1] to array[2]..array[size-1]. There's no real reason to
    // do this, other than to demonstrate the power of references.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(array[0]);
        array[i] += array[1];
    }
}

优化编译器可能会意识到我们正在访问[0]和[1]相当多的一堆。它希望优化算法:

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Do the same thing as above, but instead of accessing array[1]
    // all the time, access it once and store the result in a register,
    // which is much faster to do arithmetic with.
    register int a0 = a[0];
    register int a1 = a[1]; // access a[1] once
    for (int i = 2; i < (int)size; i++) {
        maybeModify(a0); // Give maybeModify a reference to a register
        array[i] += a1;  // Use the saved register value over and over
    }
    a[0] = a0; // Store the modified a[0] back into the array
}

为了进行这样的优化,需要证明在调用期间没有任何东西可以改变数组[1]。这很容易做到。我永远不会少于2,所以array [i]永远不能引用数组[1]。 maybeModify()被赋予a0作为参考(别名数组[0])。因为没有“引用”算法,编译器只需要证明maybeModify永远不会得到x的地址,并且它已经证明没有任何改变数组[1]。

它还必须证明,当我们在a0中有一个临时寄存器副本时,未来的调用无法读取/写入[0]。这通常是微不足道的,因为在很多情况下很明显,引用永远不会存储在像类实例这样的永久结构中。

现在用指针做同样的事情

void maybeModify(int* x); // May modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Same operation, only now with pointers, making the
    // optimization trickier.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(&(array[0]));
        array[i] += array[1];
    }
}

行为是一样的;只是现在很难证明maybeModify不会修改数组[1],因为我们已经给它一个指针;这只猫已经不在了。现在它必须做更加困难的证明:对maybeModify的静态分析证明它永远不会写入&amp; x + 1.它还必须证明它永远不会保存可以引用数组[0]的指针,同样棘手。

现代编译器在静态分析方面越来越好,但帮助它们并使用引用总是很好。

当然,除非进行这样聪明的优化,否则编译器确实会在需要时将引用转换为指针。

编辑:发布此答案五年后,我发现了一个实际的技术差异,其中引用不同于查看相同寻址概念的不同方式。引用可以以指针不能的方式修改临时对象的生命周期。

F createF(int argument);

void extending()
{
    const F& ref = createF(5);
    std::cout << ref.getArgument() << std::endl;
};

通常,临时对象(例如由createF(5)调用创建的对象)会在表达式的末尾被销毁。但是,通过将该对象绑定到引用ref,C ++将延长该临时对象的生命周期,直到ref超出范围。

答案 7 :(得分:63)

实际上,引用并不像指针。

编译器保持对变量的“引用”,将名称与内存地址相关联;这是在编译时将任何变量名称转换为内存地址的工作。

创建引用时,只告诉编译器为指针变量指定另一个名称;这就是为什么引用不能“指向null”,因为变量不能,而不是。

指针是变量;它们包含其他变量的地址,或者可以为null。重要的是指针有一个值,而引用只有一个它正在引用的变量。

现在对实际代码的一些解释:

int a = 0;
int& b = a;

在这里,您不是要创建指向a的另一个变量;您只是在保存a值的内存内容中添加另一个名称。此内存现在有两个名称ab,可以使用任一名称进行处理。

void increment(int& n)
{
    n = n + 1;
}

int a;
increment(a);

调用函数时,编译器通常会为要复制的参数生成内存空间。函数签名定义了应该创建的空格,并给出了应该用于这些空间的名称。将参数声明为引用只是告诉编译器使用输入变量内存空间而不是在方法调用期间分配新的内存空间。说你的函数将直接操作在调用范围中声明的变量似乎很奇怪,但是请记住,在执行编译代码时,没有更多的范围;只有简单的平面内存,你的功能代码可以操纵任何变量。

现在可能存在编译器在编译时可能无法知道引用的情况,例如使用extern变量时。因此,引用可能会也可能不会被实现为底层代码中的指针。但是在我给你的例子中,它很可能不会用指针实现。

答案 8 :(得分:38)

引用永远不能是NULL

答案 9 :(得分:33)

虽然引用和指针都用于间接访问另一个值,但引用和指针之间存在两个重要区别。第一个是引用始终引用一个对象:在不初始化引用的情况下定义引用是错误的。赋值行为是第二​​个重要区别:分配给引用会更改引用所绑定的对象;它不会重新绑定对另一个对象的引用。初始化后,引用始终引用相同的基础对象。

考虑这两个程序片段。在第一个中,我们将一个指针指向另一个:

int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2;    // pi now points to ival2

分配后,ival,pi发现的对象保持不变。赋值会更改pi的值,使其指向不同的对象。现在考虑一个类似的程序,它分配两个引用:

int &ri = ival, &ri2 = ival2;
ri = ri2;    // assigns ival2 to ival

此赋值更改ival,即ri引用的值,而不是引用本身。在赋值之后,两个引用仍然引用它们的原始对象,并且这些对象的值现在也是相同的。

答案 10 :(得分:27)

如果您不熟悉以抽象甚至学术方式学习计算机语言,那么语义上的差异可能会显得深奥。

在最高级别,引用的想法是它们是透明的“别名”。您的计算机可能会使用一个地址来使它们工作,但您不应该担心:您应该将它们视为现有对象的“另一个名称”,并且语法反映了这一点。它们比指针更严格,因此当您要创建悬空引用时,编译器可以更可靠地警告您,而不是在您创建悬空指针时。

除此之外,指针和引用之间当然存在一些实际差异。使用它们的语法明显不同,你不能“重新定位”引用,引用虚无,或指向引用。

答案 11 :(得分:22)

引用是另一个变量的别名,而指针包含变量的内存地址。引用通常用作函数参数,以便传递的对象不是副本而是对象本身。

    void fun(int &a, int &b); // A common usage of references.
    int a = 0;
    int &b = a; // b is an alias for a. Not so common to use. 

答案 12 :(得分:18)

它占用多少空间并不重要,因为你实际上看不到任何会占用空间的任何副作用(没有执行代码)。

另一方面,引用和指针之间的一个主要区别是,赋值给const引用的临时值一直存在,直到const引用超出范围。

例如:

class scope_test
{
public:
    ~scope_test() { printf("scope_test done!\n"); }
};

...

{
    const scope_test &test= scope_test();
    printf("in scope\n");
}

将打印:

in scope
scope_test done!

这是允许ScopeGuard工作的语言机制。

答案 13 :(得分:18)

引用不是给某些内存的另一个名称。它是一个不可变的指针,在使用时会自动取消引用。基本上归结为:

int& j = i;

内部变为

int* const j = &i;

答案 14 :(得分:18)

这是基于tutorial。写的更清楚:

>>> The address that locates a variable within memory is
    what we call a reference to that variable. (5th paragraph at page 63)

>>> The variable that stores the reference to another
    variable is what we call a pointer. (3rd paragraph at page 64)

简单地记住,

>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)

更重要的是,因为我们可以参考几乎任何指针教程,指针是指针算法支持的对象,它使指针类似于数组。

请看以下陈述,

int Tom(0);
int & alias_Tom = Tom;

alias_Tom可以理解为alias of a variable(与typedef不同,alias of a typeTom。也可以忘记这样的语句的术语是创建Tom的引用。

答案 15 :(得分:15)

我使用引用,除非我需要其中任何一个:

  • 空指针可以用作 哨兵价值,往往是一种廉价的方式 避免函数重载或使用 布尔。

  • 您可以对指针进行算术运算。 例如,p += offset;

答案 16 :(得分:15)

在C ++中可以引用指针,但反过来不可能意味着指向引用的指针是不可能的。对指针的引用提供了更清晰的语法来修改指针。 看看这个例子:

#include<iostream>
using namespace std;

void swap(char * &str1, char * &str2)
{
  char *temp = str1;
  str1 = str2;
  str2 = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap(str1, str2);
  cout<<"str1 is "<<str1<<endl;
  cout<<"str2 is "<<str2<<endl;
  return 0;
}

考虑上述程序的C版本。在C中你必须使用指向指针(多个间接),它会导致混淆,程序可能看起来很复杂。

#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
  char *temp = *str1_ptr;
  *str1_ptr = *str2_ptr;
  *str2_ptr = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap1(&str1, &str2);
  printf("str1 is %s, str2 is %s", str1, str2);
  return 0;
}

有关指针引用的更多信息,请访问以下内容:

正如我所说,指向引用的指针是不可能的。请尝试以下程序:

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}

答案 17 :(得分:14)

我没有看到任何人提到过的指针和引用之间存在一个根本区别:引用在函数参数中启用了pass-by-reference语义。指针虽然起初不可见,但它们不会:它们只提供按值传递的语义。这在this article中已经很好地描述了。

此致 &安培; rzej

答案 18 :(得分:13)

存在增加混淆的风险,我想提出一些输入,我确定它主要取决于编译器如何实现引用,但在gcc的情况下,引用只能指向堆栈上的变量实际上并不正确,例如:

#include <iostream>
int main(int argc, char** argv) {
    // Create a string on the heap
    std::string *str_ptr = new std::string("THIS IS A STRING");
    // Dereference the string on the heap, and assign it to the reference
    std::string &str_ref = *str_ptr;
    // Not even a compiler warning! At least with gcc
    // Now lets try to print it's value!
    std::cout << str_ref << std::endl;
    // It works! Now lets print and compare actual memory addresses
    std::cout << str_ptr << " : " << &str_ref << std::endl;
    // Exactly the same, now remember to free the memory on the heap
    delete str_ptr;
}

哪个输出:

THIS IS A STRING
0xbb2070 : 0xbb2070

如果您注意到内存地址完全相同,则意味着引用成功指向堆上的变量!现在,如果你真的想变得怪异,这也有效:

int main(int argc, char** argv) {
    // In the actual new declaration let immediately de-reference and assign it to the reference
    std::string &str_ref = *(new std::string("THIS IS A STRING"));
    // Once again, it works! (at least in gcc)
    std::cout << str_ref;
    // Once again it prints fine, however we have no pointer to the heap allocation, right? So how do we free the space we just ignorantly created?
    delete &str_ref;
    /*And, it works, because we are taking the memory address that the reference is
    storing, and deleting it, which is all a pointer is doing, just we have to specify
    the address with '&' whereas a pointer does that implicitly, this is sort of like
    calling delete &(*str_ptr); (which also compiles and runs fine).*/
}

哪个输出:

THIS IS A STRING

因此引用是引擎盖下的指针,它们都只是存储一个内存地址,地址所指向的是无关紧要的,如果我调用std :: cout&lt;&lt; str_ref;在调用delete&amp; str_ref之后?嗯,显然它编译得很好,但是在运行时导致分段错误,因为它不再指向有效变量,我们基本上有一个仍然存在的破坏引用(直到它超出范围),但是没用。

换句话说,引用只不过是一个指针,它将指针机制抽象出来,使其更安全,更容易使用(没有意外的指针数学,没有混淆&#39;。&#39;和## 39; - &gt;&#39;等),假设你不尝试像我上面的例子那样的废话;)

现在无论编译器如何处理引用,始终都会有一些指针,因为引用必须引用对于特定内存地址的特定变量,为了使其按预期工作,没有解决这个问题(因此术语&#39;引用&#39;)。

使用引用记住的唯一主要规则是它们必须在声明时定义(除了头中的引用外,在这种情况下它必须在构造函数中定义,在构造它所包含的对象之后构建它已经太晚了。)

请记住,我上面的例子就是这样,展示参考资料的例子,你永远不会想要以这些方式使用参考!为了正确使用参考文献,这里已有很多答案可以解决问题

答案 19 :(得分:12)

另一个区别是你可以指向一个void类型的指针(它意味着指向任何东西的指针),但禁止引用void。

int a;
void * p = &a; // ok
void & p = a;  //  forbidden

我不能说我对这种特殊的差异感到非常满意。我更倾向于允许带有地址的含义引用,以及引用的相同行为。它允许使用引用定义一些C库函数的等价物,如memcpy。

答案 20 :(得分:10)

该程序可能有助于理解问题的答案。这是一个引用“j”的简单程序和一个指向变量“x”的指针“ptr”。

#include<iostream>

using namespace std;

int main()
{
int *ptr=0, x=9; // pointer and variable declaration
ptr=&x; // pointer to variable "x"
int & j=x; // reference declaration; reference to variable "x"

cout << "x=" << x << endl;

cout << "&x=" << &x << endl;

cout << "j=" << j << endl;

cout << "&j=" << &j << endl;

cout << "*ptr=" << *ptr << endl;

cout << "ptr=" << ptr << endl;

cout << "&ptr=" << &ptr << endl;
    getch();
}

运行程序并查看输出,你就会明白。

另外,请花10分钟观看此视频:https://www.youtube.com/watch?v=rlJrrGV0iOg

答案 21 :(得分:10)

引用的另一个有趣用途是提供用户定义类型的默认参数:

class UDT
{
public:
   UDT() : val_d(33) {};
   UDT(int val) : val_d(val) {};
   virtual ~UDT() {};
private:
   int val_d;
};

class UDT_Derived : public UDT
{
public:
   UDT_Derived() : UDT() {};
   virtual ~UDT_Derived() {};
};

class Behavior
{
public:
   Behavior(
      const UDT &udt = UDT()
   )  {};
};

int main()
{
   Behavior b; // take default

   UDT u(88);
   Behavior c(u);

   UDT_Derived ud;
   Behavior d(ud);

   return 1;
}

默认flavor使用'绑定const引用到引用的临时'方面。

答案 22 :(得分:10)

直接答案

C ++中的引用是什么? 不是对象类型的某些特定类型的实例。

C ++中的指针是什么? 是对象类型的某些特定类型的实例。

来自the ISO C++ definition of object type

  

对象类型是(可能是 cv 限定的)类型,不是函数类型,引用类型和 cv 无效。

重要的是要知道,对象类型是C ++中Universe类型的顶级类别。参考也是顶级类别。 但指针不是。

指针和引用一起in the context of compound type被提及。这基本上是由于声明语语法的性质所继承的,该声明语语法是从(并扩展的)C继承的,它没有引用。 (此外,自C ++ 11起,引用的声明符不止一种,而指针仍是“唯一类型的”:& + &&*。)因此,起草了一种特定于语言的语言在这种情况下,通过“扩展”使用具有相似样式的C有点合理。 (我仍然会争辩说,声明符的语法浪费了很多的句法表现力,使人类用户和实现都感到沮丧。因此,它们都不具有内置的条件,这是关于PL设计的完全不同的话题。)

否则,可以将指针限定为带有引用的特定类型的类型是无关紧要的。除了语法相似性以外,它们仅共享很少的通用属性,因此在大多数情况下无需将它们放在一起。

请注意,以上声明仅提及“指针”和“引用”作为类型。关于它们的实例(例如变量),存在一些有趣的问题。误解也太多了。

顶级类别的差异已经可以揭示许多不直接与指针相关的具体差异:

  • 对象类型可以具有顶级cv限定符。引用不能。
  • 根据the abstract machine语义,各种对象类型确实占据了存储空间。参考不必占用存储空间(有关详细信息,请参见下面有关误解的部分)。
  • ...

关于引用的其他一些特殊规则:

  • Compound declarators are more restrictive on references.
  • 参考可以collapse
    • 基于模板参数推导过程中参考折叠的&&参数(作为“转发参考”)的特殊规则允许参数"perfect forwarding"
  • 参考文献在初始化时有特殊的规则。通过扩展,声明为引用类型的变量的生存期可以与普通对象不同。
    • 顺便说一句,诸如std::initializer_list的初始化之类的其他一些上下文遵循一些类似的参考生存期扩展规则。这是另一种蠕虫。
  • ...

误解

Syntactic sugar

  

我知道引用是语法糖,因此代码更易于读写。

从技术上讲,这是完全错误的。引用不是C ++中任何其他功能的语法糖,因为引用不能在没有语义差异的情况下被其他功能完全取代。

(类似地, lambda表达式是C ++中任何其他功能的 not 语法糖,因为无法使用{{3} },这很重要,因为此类变量的初始化顺序可能很重要。)

在严格意义上,

C ++仅包含几种语法糖。一个实例(从C继承)是内置的(非重载)运算符[]the declaration order of the captured variables

存储

  

因此,指针和引用都使用相同的内存量。

以上陈述完全是错误的。为避免这种误解,请改用ISO C ++规则:

来自is defined exactly having same semantic properties of specific forms of combination over built-in operator unary * and binary +

  

...对象在其构造期间,整个生命周期和破坏期间都占据一个存储区域。 ...

来自[intro.object]/1

  

未确定引用是否需要存储。

请注意,这些是语义属性。

语用学

即使就语言设计的意义而言,即使指针没有足够的资格将其与引用放在一起,但仍有一些参数使在其他情况下(例如,在对参数进行选择时)在它们之间进行选择值得商bat类型。

但这还不是全部。我的意思是,除了要考虑的指针还是引用之外,还有很多事情需要考虑。

如果您不必坚持这种过分具体的选择,则在大多数情况下答案很短:您不必使用指针,因此不必。指针通常很糟糕,因为它们暗示了太多您不期望的事情,并且它们将依赖太多隐式假设,从而破坏了代码的可维护性和(甚至)可移植性。 不必要地依赖指针绝对是一种不好的风格,应该从现代C ++的角度避免。重新考虑您的目的,您最终会发现指针是最后一种功能 >在大多数情况下。

  • 有时语言规则明确要求使用特定类型。如果要使用这些功能,请遵守规则。
    • 复制构造函数需要特定类型的 cv -&引用类型作为第一参数类型。 (通常应该是const合格的。)
    • 移动构造函数需要特定类型的 cv -&&引用类型作为第一参数类型。 (通常应该没有限定词。)
    • 运算符的特定重载需要引用或非引用类型。例如:
      • 作为特殊成员函数重载的operator=需要类似于复制/移动构造函数的第一个参数的引用类型。
      • 后缀++需要虚拟int
      • ...
  • 如果您知道值传递(即使用非引用类型)就足够了,请直接使用它,尤其是在使用支持C ++ 17强制复制省略的实现时。 (警告:但是,详尽的理由可能是[dcl.ref]/4。)
  • 如果您想使用所有权来处理某些句柄,请使用unique_ptrshared_ptr之类的智能指针(如果您要求自己是 opaque ,甚至可以单独使用自制的指针) ),而不是原始指针。
  • 如果您正在某个范围内进行某些迭代,请使用迭代器(或标准库尚未提供的某些范围),而不是原始指针,除非您确信原始指针会做得更好(例如,减少头文件的依赖)在非常特殊的情况下。
  • 如果您知道传递值就足够了,并且想要一些显式的可为空的语义,请使用std::optional之类的包装器,而不要使用原始指针。
  • 如果您出于上述原因知道传递值不理想,并且您不希望使用可为空的语义,请使用{lvalue,rvalue,forwarding} -references。
  • 即使您确实想要像传统指针这样的语义,通常也更合适一些,例如Library Fundamental TS中的observer_ptr

唯一的例外无法用当前语言解决:

  • 在实现上述智能指针时,可能必须处理原始指针。
  • 特定的语言互操作例程需要指针,例如operator new。 (但是, cv -void*与普通对象指针相比还是有很大不同,并且更安全,因为它可以排除意外的指针算术,除非您依靠{{1} },例如GNU。)
  • 可以从lambda表达式转换函数指针而无需捕获,而函数引用则不能。对于这种情况,即使您故意不希望使用可为空的值,也必须在非通用代码中使用函数指针。

因此,在实践中,答案非常明显:如有疑问,请避免使用指针。仅在出于非常明显的原因没有其他更合适的理由时才需要使用指针。除了上面提到的一些例外情况外,此类选择几乎总是不完全是特定于C ++的(但可能是特定于语言实现的)。这样的实例可以是:

  • 您必须使用旧式(C)API。
  • 您必须满足特定C ++实现的ABI要求。
  • 您必须基于特定实现的假设,在运行时与不同的语言实现(包括各种程序集,语言运行时和某些高级客户端语言的FFI)进行互操作。
  • 在某些极端情况下,您必须提高翻译(编译和链接)的效率。
  • 在某些极端情况下,您必须避免符号膨胀。

语言中性警告

如果您通过very complicated看到问题,很可能是在错误的地方。

C ++中的引用相当“奇怪”,因为它本质上不是一流的:some Google search result (not specific to C++),因此它们没有机会支持某些一流的操作,例如独立地成为they will be treated as the objects or the functions being referred to的左操作数引用对象的类型。其他语言可能或可能没有类似的参考限制。

C ++中的引用可能不会保留不同语言之间的含义。例如,引用通常并不暗示像C ++那样的值具有非null属性,因此此类假设在某些其他语言中可能不起作用(并且您会很容易找到反例,例如Java,C#,...)。

通常,在不同编程语言中的引用之间仍然存在一些公共属性,但让我们将其留给SO中的其他问题。

(附带说明:这个问题可能比涉及任何“ C样”语言(例如the member access operator的时间要早​​)。

答案 23 :(得分:10)

此外,作为内联函数的参数的引用可能与指针的处理方式不同。

void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
    int testptr=0;
    increment(&testptr);
}
void increftest()
{
    int testref=0;
    increment(testref);
}

在内联指针版本1时,许多编译器实际上会强制写入内存(我们正在明确地获取地址)。但是,他们会将参考文献保留在更优化的寄存器中。

当然,对于没有内联的函数,指针和引用会生成相同的代码,如果函数没有修改并返回它们,那么通过值传递内在函数而不是引用它总是更好。

答案 24 :(得分:9)

我觉得还有另外一点,这里没有涉及。

与指针不同,引用与它们引用的对象的语法等效,即可以应用于对象的任何操作都可以用于引用,并且具有完全相同的语法(例外是当然是初始化)。

虽然这可能看起来很肤浅,但我相信这个属性对于许多C ++功能至关重要,例如:

  • 模板的。由于模板参数是鸭型的,因此类型的语法属性非常重要,因此TT&通常可以使用相同的模板。
    (或std::reference_wrapper<T>仍然依赖于隐式演员 至T&
    涵盖T&T&&的模板更为常见。

  • 左值。考虑语句str[0] = 'X';没有引用它只适用于c-strings(char* str)。通过引用返回字符允许用户定义的类具有相同的表示法。

  • 复制构造函数。从语法上讲,将对象传递给复制构造函数是有意义的,而不是指向对象的指针。但是,复制构造函数无法通过值获取对象 - 这将导致对同一复制构造函数的递归调用。这使得引用成为唯一的选择。

  • 运算符重载。通过引用,可以将间接引入操作员调用 - 比如operator+(const T& a, const T& b),同时保留相同的中缀符号。这也适用于常规的重载功能。

这些要点赋予了C ++和标准库的相当大的一部分,因此这是引用的一个主要属性。

答案 25 :(得分:8)

指针和引用之间存在非常重要的非技术差异:通过指针传递给函数的参数比通过非const引用传递给函数的参数更加明显。例如:

Output

回到C,看起来像propChange的调用只能通过值传递,所以它肯定无法修改void fn1(std::string s); void fn2(const std::string& s); void fn3(std::string& s); void fn4(std::string* s); void bar() { std::string x; fn1(x); // Cannot modify x fn2(x); // Cannot modify x (without const_cast) fn3(x); // CAN modify x! fn4(&x); // Can modify x (but is obvious about it) } ;修改一个参数,你需要传递一个指针fn(x)。因此,如果参数前面没有x,那么您就知道它不会被修改。 (相反,fn(&x)意味着修改,不是真的,因为你有时必须通过&指针传递大的只读结构。)

有些人认为,在阅读代码时这是一个非常有用的功能,指针参数应该始终用于可修改的参数而不是非&引用,即使函数从不期望const。也就是说,那些人认为不应该允许像const这样的功能签名。 Google's C++ style guidelines就是一个例子。

答案 26 :(得分:8)

也许一些比喻会有所帮助; 在桌面屏幕空间的上下文中 -

  • 参考要求您指定实际窗口。
  • 指针需要屏幕上一块空间的位置,确保它包含零个或多个该窗口类型的实例。

答案 27 :(得分:6)

指针和引用之间的区别

指针可以初始化为0而引用不能。实际上,引用也必须引用一个对象,但指针可以是空指针:

int* p = 0;

但我们不能拥有int& p = 0;int& p=5 ;

事实上,为了正确地完成它,我们必须首先声明和定义一个对象,然后我们可以对该对象进行引用,因此前面代码的正确实现将是:

Int x = 0;
Int y = 5;
Int& p = x;
Int& p1 = y;

另一个重要的一点是,我们可以在没有初始化的情况下进行指针的声明,但是在引用的情况下不能做这样的事情,它必须总是引用变量或对象。然而,这种指针的使用是有风险的,所以通常我们检查指针是否实际指向某事物。如果是引用,则不需要进行此类检查,因为我们已经知道在声明期间引用对象是必需的。

另一个区别是指针可以指向另一个对象,但是引用总是引用同一个对象,让我们举个例子:

Int a = 6, b = 5;
Int& rf = a;

Cout << rf << endl; // The result we will get is 6, because rf is referencing to the value of a.

rf = b;
cout << a << endl; // The result will be 5 because the value of b now will be stored into the address of a so the former value of a will be erased

另一点:当我们有一个类似STL模板的模板时,这种类模板将始终返回引用而不是指针,以便使用operator []轻松读取或分配新值:

Std ::vector<int>v(10); // Initialize a vector with 10 elements
V[5] = 5; // Writing the value 5 into the 6 element of our vector, so if the returned type of operator [] was a pointer and not a reference we should write this *v[5]=5, by making a reference we overwrite the element by using the assignment "="

答案 28 :(得分:6)

引用是const指针。 df[grep(paste(terms, collapse = '|'), tolower(df$TRAIT)), ] int * const a = &b相同。这就是为什么没有const引用的原因,因为它已经是const,而对const的引用是int& a = b。当您使用-O0进行编译时,在两种情况下,编译器都会将b的地址放置在堆栈上,并且作为类的成员,它在堆栈/堆中的对象中也将存在,与声明a const指针。使用-Ofast,可以自由地对此进行优化。

与const指针不同,无法获取引用本身的地址,因为它将被解释为其引用的变量的地址。因此,始终会优化引用,但是如果程序绝对需要-Ofast上的const指针的地址,即您打印地址,则const指针将被放置在堆栈中。

const int * const a

作为对象的成员,它们在-O0到-Ofast上是相同的。

#include <iostream>

int main() {
  int a =1;
  int* b = &a;
  std::cout << b ;
}

int main() {
  int a =1;
  int& b = a;
  std::cout << &b ;
}

they both have the same assembly output
-Ofast:
main:
        sub     rsp, 24
        mov     edi, OFFSET FLAT:_ZSt4cout
        lea     rsi, [rsp+12]
        mov     DWORD PTR [rsp+12], 1
        call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<void const*>(void const*)
        xor     eax, eax
        add     rsp, 24
        ret

-O0:
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-12], 1
        lea     rax, [rbp-12]
        mov     QWORD PTR [rbp-8], rax
        mov     rax, QWORD PTR [rbp-8]
        mov     rsi, rax
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(void const*)
        mov     eax, 0
        leave
        ret

答案 29 :(得分:6)

用简单的话来说,我们可以说引用是变量的替代名称,而 指针是一个变量,其中包含另一个变量的地址。 例如

int a = 20;
int &r = a;
r = 40;  /* now the value of a is changed to 40 */

int b =20;
int *ptr;
ptr = &b;  /*assigns address of b to ptr not the value */

答案 30 :(得分:5)

不同之处在于,非常量指针变量(不要与指向常量的指针混淆)可能会在程序执行期间的某个时间发生变化,需要使用指针语义(&amp;,*)运算符,而引用可以只在初始化时设置(这就是为什么你只能在构造函数初始化列表中设置它们,但不能以其他方式设置它们)并使用普通值访问语义。基本上引用了引用,以支持运算符重载,正如我在一本非常古老的书中所读到的那样。正如有人在这个线程中所说 - 指针可以设置为0或任何你想要的值。 0(NULL,nullptr)表示指针初始化为nothing。取消引用空指针是一个错误。但实际上指针可能包含一个不指向某个正确内存位置的值。引用依次尝试不允许用户初始化对无法引用的引用的引用,因为您始终为其提供正确类型的rvalue。尽管有很多方法可以将引用变量初始化为错误的内存位置 - 但是最好不要深入研究细节。在机器级别,指针和引用均匀地工作 - 通过指针。让我们在必要的参考文献中说出语法糖。 rvalue引用与此不同 - 它们自然是堆栈/堆对象。

答案 31 :(得分:4)

以下答案和链接的摘要:

  1. 在绑定后无法重新分配引用的情况下,可以多次重新分配指针。
  2. 指针无法指向任何地方(NULL),而引用始终指向对象。
  3. 您不能像使用指针那样获取引用的地址。
  4. 没有“引用算术”(但是您可以像引用&obj + 5那样使用引用指向的对象的地址并对其进行指针算术)。

为澄清误解:

C ++标准非常小心,以避免指示编译器可能如何 实现引用,但是每个C ++编译器都实现 引用作为指针。也就是说,这样的声明:

int &ri = i;

如果尚未完全优化 ,则分配相同的存储量 作为指针,并放置地址 i放入该存储空间。

因此,指针和引用都使用相同的内存量。

作为一般规则,

  • 在函数参数和返回类型中使用引用,以提供有用的自记录界面。
  • 使用指针实现算法和数据结构。

有趣的读物:

答案 32 :(得分:3)

关于引用和指针的一些关键相关细节

指针

  • 使用一元后缀声明符 *
  • 声明指针变量
  • 为指针对象分配了一个地址值,例如,通过分配给数组对象,使用&一元前缀运算符分配给对象的地址,或分配给另一个指针对象的值< / li>
  • 可以多次重新分配指向不同对象的指针
  • 指针是保存分配的地址的变量。它占用的内存存储量等于目标机器体系结构的地址大小
  • 可以通过例如增量或加法运算符在数学上操纵指针。因此,可以使用指针等进行迭代。
  • 要获取或设置指针引用的对象的内容,必须使用一元前缀运算符 *对其进行取消引用

参考文献

  • 引用必须在声明时进行初始化。
  • 引用使用一元后缀声明符&。
  • 声明。
  • 初始化引用时,无需使用一元前缀运算符
  • ,即可使用它们将直接引用的对象的名称。
  • 初始化后,引用不能通过赋值或算术操作指向其他内容
  • 无需取消引用的引用即可获取或设置所引用对象的内容
  • 对引用的赋值操作(在初始化之后)操纵它所指向的对象的内容,而不是引用本身(不会更改其指向的位置)
  • 对引用的算术运算会操纵它指向的对象的内容,而不是引用本身(不会改变指向的位置)
  • 在几乎所有实现中,引用实际上都作为地址存储在所引用对象的内存中。因此,就像指针对象一样,它占用的内存等于目标机器体系结构的地址大小。

即使指针和引用的实现方式大致相同,“编译器”也将它们区别对待,从而导致上述所有差异。

文章

我最近写的一篇文章的内容比这里显示的要详细得多,对于这个问题,特别是关于内存中的事情如何发生,应该会很有帮助:

Arrays, Pointers and References Under the Hood In-Depth Article

答案 33 :(得分:2)

我总是通过C ++核心指南中的this规则决定:

  

优先选择T&amp; T&amp;当“无论证”是一个有效的选择时

答案 34 :(得分:2)

我对引用和指针进行了类比,将引用视为对象的另一个名称,将指针视为对象的地址。

// receives an alias of an int, an address of an int and an int value
public void my_function(int& a,int* b,int c){
    int d = 1; // declares an integer named d
    int &e = d; // declares that e is an alias of d
    // using either d or e will yield the same result as d and e name the same object
    int *f = e; // invalid, you are trying to place an object in an address
    // imagine writting your name in an address field 
    int *g = f; // writes an address to an address
    g = &d; // &d means get me the address of the object named d you could also
    // use &e as it is an alias of d and write it on g, which is an address so it's ok
}

答案 35 :(得分:1)

如果您遵循传递给函数的参数的约定,则可以使用引用和指针之间的区别。 const引用用于传递到函数的数据,而指针则用于从函数传递的数据。在其他语言中,您可以使用诸如inout之类的关键字来显式注释。在C ++中,您可以(按照惯例)声明等效项。例如,

void DoSomething(const Foo& thisIsAnInput, Foo* thisIsAnOutput)
{
   if (thisIsAnOuput)
      *thisIsAnOutput = thisIsAnInput;
}

将引用用作输入,将指针用作输出是Google style guide的一部分。

答案 36 :(得分:0)

指针是一个变量,它保存另一个变量的内存地址,而引用是现有变量的别名。 (已存在变量的另一个名称)

  1. 一个指针可以初始化为,

    int b = 15;
    int *q = &b;
    要么 整数 *q; q = &b; 作为参考,

    int b=15; int &c=b;

(一步声明和初始化)

  1. 指针可以赋值为null,但引用不能
  2. 可以对指针执行各种算术运算,而没有所谓的参考算术。
  3. 指针可以重新赋值,但引用不能
  4. 指针在堆栈上有自己的内存地址和大小,而引用共享相同的内存地址

答案 37 :(得分:0)

pointer(*)的基本含义是“地址的值”,这意味着您提供的任何地址都将在该地址给出值。更改地址后,它将赋予新值,而引用变量用于引用任何特定变量,并且将来不能更改为引用任何其他变量。

答案 38 :(得分:0)

“我知道引用是语法糖,因此代码更易于读写”

这个。引用不是实现指针的另一种方法,尽管它涵盖了巨大的指针用例。指针是一种数据类型,即通常指向实际值的地址。但是,可以将其设置为零,也可以使用地址算术等将其设置为地址后的几个位置。对于具有自己值的变量,引用是“语法糖”。

C仅具有按值传递的语义。获取变量引用的数据地址并将其发送给函数是通过“引用”传递的一种方式。引用通过“引用”原始数据位置本身在语义上简化了操作。所以:

int x = 1;
int *y = &x;
int &z = x;

Y是一个int指针,指向x的存储位置。 X和Z指向相同的存储位置(堆栈或堆)。

很多人都在谈论两者(指针和引用)之间的区别,就好像它们是相同的东西,具有不同的用法。它们根本不一样。

1)“在绑定后不能重新分配引用的情况下,可以多次重新分配指针。” -指针是指向数据的地址数据类型。引用是数据的另一个名称。因此,您可以“重新分配”参考。您只是无法重新分配它引用的数据位置。就像您无法更改“ x”所指的数据位置一样,您也无法将其更改为“ z”。

x = 2;
*y = 2;
z = 2;

一样。这是重新分配。

2)“指针不能指向任何地方(NULL),而引用始终指向对象” –再次感到困惑。引用只是对象的另一个名称。空指针意味着(语义上)它没有引用任何东西,而引用是通过说它是'x'的另一个名字而创建的。自

3)“您不能像使用指针一样获取引用的地址” –是的。再次感到困惑。如果您试图查找用作引用的指针的地址,那就是一个问题-原因是引用不是指向该对象的指针。他们是对象。这样就可以获取对象的地址,并且可以获取指针的地址。因为它们都在获取数据地址(一个对象在内存中的位置,另一个是指向对象在内存中的位置的指针)。

int *yz = &z; -- legal
int **yy = &y; -- legal

int *yx = &x; -- legal; notice how this looks like the z example.  x and z are equivalent.

4)“再也没有“引用算术””了-再次感到困惑-由于上面的示例中z是对x的引用,因此都是整数,因此“引用”算术意味着将值加1由x引用。

x++;
z++;

*y++;  // what people assume is happening behind the scenes, but isn't. it would produce the same results in this example.
*(y++);  // this one adds to the pointer, and then dereferences it.  It makes sense that a pointer datatype (an address) can be incremented.  Just like an int can be incremented. 

答案 39 :(得分:0)

C中“&”和“ *”运算符有什么区别 &是C中的一元运算符,它返回传递的操作数的内存地址。这也称为操作员地址。 <> *是一元运算符,它返回由指针变量指向的对象的值。它被称为运算符值。它也用于声明指针变量。

答案 40 :(得分:0)

除了所有答案,

您可以使用引用实现运算符重载:

my_point operator+(const my_point& a, const my_point& b)
{
  return { a.x + b.x, a.y + b.y };
}

将参数用作值将创建原始参数的临时副本,并且由于指针的运算,使用指针将不会调用此函数。

答案 41 :(得分:0)

Taryn♦说:

  

您无法使用指针获取引用的地址。

其实你可以。

我引用an answer on another question

  

The C++ FAQ说得最好:

     

与指针不同,一旦引用绑定到对象,它就不能“重新”到另一个对象。引用本身不是一个对象(它没有标识;获取引用的地址会给你引用的地址;记住:引用是它的引用)。