指针的引用 - c++空引用



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

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

但有什么区别?

以下答案和链接摘要:

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

澄清一个误解:

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

int &ri = i;

如果它没有完全优化 ,则分配与指针相同的存储量,并将i的地址放入该存储器中。

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

作为基本规则,

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

有趣的读物:


Answer #1

什么是C ++参考( 适用于C程序员

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

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

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

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

请考虑C ++ FAQ中的以下语句:

尽管引用通常是使用底层汇编语言中的地址实现的,但请不要将引用视为指向对象的有趣外观指针。 参考对象。 它不是指向对象的指针,也不是对象的副本。 这对象。

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

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

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

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


Answer #2

指针和引用之间的区别

指针可以初始化为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 "="

Answer #3

与流行的观点相反,可能有一个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 尝试将代码移植到另一个平台时暴露未定义的行为


Answer #4

你忘记了最重要的部分:

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

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


Answer #5

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

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

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


Answer #6

如果你想变得非常迂腐,你可以用引号做一件事,你不能用指针做:延长临时对象的生命周期。 在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引用绑定到临时对象,也不能为此处获取其地址。


Answer #7

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

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


Answer #8

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

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

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

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

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

int a = 0;
int& b = a;

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

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

int a;
increment(a);

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

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


Answer #9

引用永远不能为NULL


Answer #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引用到引用的临时'方面。


Answer #11

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

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

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

  • 模板。由于模板参数是鸭类型,一种类型的语法属性是最重要的事情,所以往往是相同的模板可以既使用TT&
    (或者std::reference_wrapper<T>仍然依赖于隐式转换T&
    模板涵盖了两者T&T&&甚至更常见。

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

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

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

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


Answer #12

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

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

回到C,看起来像fn(x)只能通过值传递的调用,所以它肯定无法修改x;修改您需要传递指针的参数fn(&x)。因此,如果参数之前没有参数,则&您知道它不会被修改。 (相反,&意味着修改,不是真的,因为你有时必须通过const指针传递大的只读结构。)

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


Answer #13

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

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

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

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

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


Answer #14

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

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

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

Answer #16

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

int& j = i;

它在内部成为

int* const j = &i;

Answer #17

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

    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. 

Answer #18

我总是决定this从C ++核心指导原则:

当“无参数”是有效选项时,首选T *而不是T.


Answer #19

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

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

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引用的值,而不是引用本身。在赋值之后,两个引用仍然引用它们的原始对象,并且这些对象的值现在也是相同的。


Answer #20

该计划可能有助于理解问题的答案。这是引用“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=rlJrrGV0iOghttps://www.youtube.com/watch?v=rlJrrGV0iOg





c++-faq