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

时间:2020-03-05 18:51:46  来源:igfitidea点击:

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

但是有什么区别呢?

以下答案和链接的摘要:

  • 可以多次分配指针,而绑定后不能重新分配引用。
  • 指针不能指向任何地方(" NULL"),而引用始终指向对象。
  • 我们不能像使用指针那样获取引用的地址。
  • 没有"引用算术"(但是我们可以使用引用指向的对象的地址,并像在&obj + 5中那样对它执行指针算术)。

为了澄清一个误解:

The C++ standard is very careful to avoid dictating how a compiler may
  implement references, but every C++ compiler implements
  references as pointers. That is, a declaration such as:

int &ri = i;

  
  if it's not optimized away entirely, allocates the same amount of storage
  as a pointer, and places the address
  of i into that storage.

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

作为基本规则,

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

有趣的读物:

  • 我一直以来最喜欢的C ++常见问题精简版。
  • 参考与指针。
  • 参考文献简介。
  • 参考和常量。

解决方案

回答

除了语法糖外,引用是一个const指针(不是指向const的指针)。我们必须在声明引用变量时建立它所引用的内容,并且以后不能更改它。

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

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

引用目标不能用UB以外的任何方式替换。

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

回答

  • 可以重新分配一个指针:
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;
  • 指针在堆栈上有自己的内存地址和大小(x86上为4字节),而引用共享相同的内存地址(带有原始变量),但也占用了堆栈上的一些空间。由于引用具有与原始变量本身相同的地址,因此可以将引用视为同一变量的另一个名称。注意:指针指向的内容可以在堆栈或者堆上。同上一个参考。我在此声明中的主张不是指针必须指向堆栈。指针只是保存内存地址的变量。此变量在堆栈上。由于引用在堆栈上有自己的空间,并且地址与引用的变量相同。有关堆栈与堆的更多信息。这意味着存在一个编译器不会告诉引用的真实地址。
int x = 0;
int &r = x;
int *p = &x;
int *p2 = &r;
assert(p == p2);
  • 我们可以拥有指向提供额外级别间接功能的指针。而引用仅提供一种间接级别。
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);
  • 指针可以直接分配为" nullptr",而引用则不能。如果我们尽力而为,并且知道如何做,则可以使引用为null的地址。同样,如果我们尽力而为,则可以有一个指向指针的引用,然后该引用可以包含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
  • 指针可以遍历数组,可以使用" ++"转到指针所指向的下一个项目,并使用" + 4"转到第五个元素。无论指针指向的对象大小是多少。
  • 指针需要用*解除引用,以访问其指向的内存位置,而引用可以直接使用。指向类/结构的指针使用"->"来访问其成员,而引用则使用"。"。
  • 指针是保存内存地址的变量。无论引用如何实现,引用都与其引用的项具有相同的内存地址。
  • 引用不能填充到数组中,而指针可以(由用户@litb提及)
  • 常量引用可以绑定到临时对象。指针不能(不是没有间接的):
const int &x = int(12); //legal C++
int *y = &int(12); //illegal to dereference a temporary.

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

回答

引用永远不能为" NULL"。

回答

与流行观点相反,可能有一个为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,因此从编译器的角度来看,比较始终为false,可以自由地将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试图将代码移植到另一个平台时公开未定义的行为。

回答

如果我们想成为真正的书呆子,则可以使用引用做某件事,而不能使用指针做这件事:延长临时对象的寿命。在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的情况下尝试此操作,则它将无法编译。我们不能将非常量引用绑定到临时对象,也不能使用它的地址。

回答

我们忘记了最重要的部分:

带指针的成员访问使用->
具有引用的成员访问使用.

foo.bar明显优于foo-> bar,就像vi明显优于Emacs一样:-)

回答

我使用引用,除非我需要以下任何一种:

  • 空指针可以用作标记值,这通常是避免函数重载或者使用布尔值的廉价方法。
  • 我们可以对指针进行算术运算。例如," p + = offset;"

回答

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

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

默认风格使用引用的"将const引用绑定到临时"方面。

回答

占用多少空间都没有关系,因为我们实际上看不到它会占用任何空间的任何副作用(不执行代码)。

另一方面,引用和指针之间的主要区别在于,分配给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运行的语言机制。

回答

实际上,引用不是真的像指针。

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

创建引用时,仅告诉编译器我们为指针变量分配了另一个名称。这就是为什么引用不能"指向null"的原因,因为变量不能是,也不能是。

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

现在对真实代码进行一些解释:

int a = 0;
int& b = a;

在这里,我们没有创建另一个指向a的变量。我们只是将另一个名称添加到包含`a'值的内存内容中。现在,该存储器具有两个名称,即" a"和" b",并且可以使用任一名称进行寻址。

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

int a;
increment(a);

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

现在,在某些情况下,例如在使用extern变量时,编译器可能无法知道引用。因此,引用可以或者可以不被实现为基础代码中的指针。但是在我给示例中,很可能不会使用指针来实现它。

回答

可以将引用视为具有自动间接指向的常量指针(不要与指向常量值的指针混淆!),即编译器将为我们应用*运算符。

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

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

C ++程序员可能不喜欢使用指针,因为它们被认为是不安全的,尽管引用实际上并没有比常量指针更安全,除非在大多数情况下,引用缺乏自动间接的便利,并带有不同的语义含义。

请考虑C ++常见问题解答中的以下声明:

Even though a reference is often implemented using an address in the
  underlying assembly language, please do not think of a reference as a
  funny looking pointer to an object. A reference is the object. It is
  not a pointer to the object, nor a copy of the object. It is the
  object.

但是,如果引用确实是对象,那么怎么会有悬挂的引用呢?在非托管语言中,引用是不可能比指针更"安全"的,通常情况下,这是一种无法跨范围可靠地别名的方法!

来自C的背景,C ++引用可能看起来有点愚蠢,但是在可能的情况下,仍然应该使用它们而不是指针:自动间接访问很方便,并且在处理RAII时引用特别有用,但并不是因为任何明显的安全优势,而是因为这样可以减少编写惯用代码的麻烦。

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

回答

另外,可以以不同于指针的方式处理作为内联函数的参数的引用。

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

许多编译器在内联指针版本一时实际上会强制写入内存(我们正在显式获取地址)。但是,它们会将参考保留在最佳寄存器中。

当然,对于未内联的函数,指针和引用会生成相同的代码,并且如果函数未修改和返回内联函数,则按值传递内在函数总是比按引用传递内在函数更好。

回答

另一个区别是我们可以具有指向void类型的指针(这意味着指向任何内容的指针),但是禁止引用void。

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

我不能说我对这种特殊的差异感到非常满意。我更希望对带有地址的任何内容进行含义性引用,否则对引用的行为相同。它将允许使用引用来定义一些等效的C库函数,例如memcpy。