惊艳干货!指针和引用的区别

2024-12-22

指针和引用在形式上很好区别,在C++中相比于指针我们更喜欢使用引用,但是它们的使用场景又极其类似,它们都能直接引用对象,对对象进行处理,那么究竟为什么会引入引用?什么时候使用指针?什么时候使用引用?这两者实在容易混淆,在此我详细介绍一下指针和引用。

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间。

1.引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。

2.引用初始化后不能被改变,指针可以改变所指的对象。

3.不存在指向空值的引用,但是存在指向空值的指针。

注意:引用作为函数参数时,会引发一定的问题,因为让引用作参数,目的就是想改变这个引用所指向地址的内容,而函数调用时传入的是实参,看不出函数的参数是正常变量,还是引用,因此可能引发错误。所以使用时一定要小心谨慎。

从概念上讲。指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。

而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。

在C++中,指针和引用经常用于函数的参数传递,然而,指针传递参数和引用传递参数是有本质上的不同的:

指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的
实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。而在引用传递过程中,
被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间
接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。引用传递和指针传递是
不同的,虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针
传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的
指针,或者指针引用。

为了进一步加深大家对指针和引用的区别,下面我从编译的角度来阐述它们之间的区别:

程序在编译时分别将指
针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为
引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

指针和引用的定义

维基百科中这样解释

指针:

在计算机科学中,指针(英语:Pointer),是编程语言中的一类数据类型及其对象或变量,用来表示或存储一个存储器地址,这个地址的值直接指向(points
to)存在该地址的对象的值。

引用:

在C++编程语言中,引用是一种简单的引用数据类型,其功能不如从C继承的指针类型,但更安全。C++引用的称谓可能会引起混淆,因为在计算机科学中,引用是一种通用的概念数据类型,指针和C++引用是特定的引用数据类型实现。

但说了和没说差不多。下面用通俗易懂的话来给概述一下。

指针

对于一个类型T,T*就是指向T的指针类型,也就是说T*类型的变量能够保存一个T类型变量的地址。

int main()

{

int i = 1;

int* p = &i;

cout << "p = " << p << endl;

cout << "i = " << i << endl;

return 0;

}

引用

引用是一个对象的别名,主要用于函数参数和返回值类型,符号X&表示X类型的引用。见下图,所示引用的含义:

指针和引用的区别

首先,引用不可以为空,但指针可以为空。前面也说过了引用是对象的别名,那么能初始化引用的前提一定是被引用的对象存在,引用为空——对象都不存在,怎么可能有别名!故定义一个引用的时候,必须初始化。如果你有一个变量是用于指向另一个对象,但是它可能为空,这时你应该使用指针;如果变量总是指向一个对象,并且这个变量一定不为空,这时你应该使用引用。如果定义一个引用变量,不初始化的话连编译都通不过(编译时错误):

int main()

{

int i = 10;

int* p;

int& r;

return 0;

}

报错:

“r”: 必须初始化引用

而声明指针并不需要初始化操作,即它可以不指向任何对象,也正因如此,指针的安全性不如引用,在使用指针前一定要进行判空操作;

引用初始化后就不能再改变指向,无论如何都只能指向初始化时引用的这个对象;但是指针就不同,指针是一个变量它可以任意改变自己的值,即任意改变指向,而指向其他对象。总的来说,就是引用不可以改变指向,但是可以改变初始化对象的内容,而指针即可以改变指向,又可以改变指向对象的内容;

例如:对指针和引用分别进行++操作,对引用执行此操作,作用对象会直接反应到引用所指向的对象,而对于指针,执行++操作作用于指针变量,会使指针指向下一个对象,而非改变指向对象的内容。

代码如下:

int main()

{

int i = 10;

int* p = &i;

int& r = i;

cout << "i = " << i << endl;

cout << "p = " << p << endl;

cout << "r = " << r << endl;

r++;

cout << "r++ operation:" << endl;

cout << "i = " << i << endl;

cout << "p = " << p << endl;

cout << "r = " << r << endl;

p++;

cout << "p++ operation:" << endl;

cout << "i = " << i << endl;

cout << "p = " << p << endl;

cout << "r = " << r << endl;

return 0;

}

可以看到对r执行++操作是直接反应到所指向的对象身上,对引用r的改变并不会是引用r的指向改变,它仍然指向i,并且会改变i的值;而如果是指针,则改变的是指针的值而非指向的对象的值。也就是说在引用初始化后对其的赋值等操作,都不会影响其指向,而是影响其指向的对象的内容。

引用的的大小是其指向的对象的大小,因为引用仅仅是一个别名;指针的大小与平台有关,在32位平台下指针大小为4个字节;

int main()

{

int i = 10;

int* p = &i;

int& r = i;

cout << "sizeofo(p) = " << sizeof(p) << endl;

cout << "sizeofo(r) = " << sizeof(r) << endl;

return 0;

}

由于我是在64位下进行程序编译的,因此指针大小为8个字节,而引用的大小是一个int的大小;

最后,引用比指针更安全。由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。const
指针虽然不能改变指向,但仍然存在空指针,并且有可能产生野指针(即多个指针指向一块内存,free掉一个指针之后,别的指针就成了野指针)。

引用在初始化过后,对引用的一切操作实际上是对它指向对象的内容的操作,而指针则是需要*操作符解引用后才能访问到被指向的对象,因此引用的使用也比指针更加的漂亮,更加直观;在初始化时也不需要&操作来取得地址;

总而言之,言而总之——它们的这些差别都可以归结为"指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名,引用不改变指向。"

const修饰的引用和指针

之前我们就知道,对于指针而言const的位置可以决定其修饰的对象是谁;那么引用呢?

常量引用和常量指针

常量指针:指向常量的指针,在定义的语句类型前加上const,表示指向的对象是常量;

定义指向常量的指针只限制指针的间接访问操作,而不能规定指针指向的值本身的操作规定性。

int main()

{

int i = 10;

const int* p = &i;

*p = 20;

return 0;

}

报错:

“p”: 不能给常量赋值

常量指针定义"const int* p=&i"告诉编译器,*p是常量,不能将*p作为左值进行操作。

常量引用:指向常量的引用,在引用定义的语句的类型前加上const,表示指向的对象是常量。与指针一样,不能对指向的对象的内容进行改变。

int main()

{

int i = 10;

const int& r = i;

r = 20;

return 0;

}

报错:

“r”: 不能给常量赋值

引用常量和指针常量

指针常量:指针中的常量;

在定义指针的语言中的变量名前加const,表示指针本身是一个常量,即指针的指向不可改变。在定义指针常量时必须进行初始化,欸,这像极了引用,是的!

引用指向的对象不可改变是引用的与生俱来的性质,因此不需要在引用的变量名前加上const。

int main()

{

int i = 10;

int j = 20;

int* const p = &i;

*p = 30;

p = &j;

return 0;

}

报错:

“p”: 不能给常量赋值

指针常量定义int* const p = &i告诉编译器,p是常量,不能作为左值进行赋值操作,但是允许对指向的对象进行修改。

常量引用常量和常量指针常量

常量指针常量:指向常量的指针常量,定义一个指向常量的指针常量,它必须要在定义时初始化。

常量指针常量定义"const int* const p=&i"告诉编译器,p和*p都是常量,他们都不能作为左值进行操作。

那么对于引用呢?引用本身具有不能修改指向的性质,因此引用的指向总是const的,所有的引用都是引用常量,即const不需要修饰引用名。程序决不能给引用本身重新赋值,使他指向另一个变量,因此引用总是const的。如果对引用应用关键字const,其作用就是使其目标称为const变量。即没有:Const
double const& a=1;只有const double& a=1;

注意:实际上const double &a 和 double const &a是一样的,都是定义了一个常量的引用。

技巧:有一个规则可以很好的区分const是修饰指针,还是修饰指针指向的数据——画一条垂直穿过指针声明的星号(*),如果const出现在线的左边,指针指向的数据为常量;如果const出现在右边,指针本身为常量。而引用本身与天俱来就是常量,即不可以改变指向。

指针和引用的实现

实际上在底层实现上引用还是有空间的,因为引用本质还是指针的方式来实现的。

int main()

{

int a = 9;

int& ra = a;

ra = 99;

int* pa = &a;

*pa = 99;

return 0;

}

int& ra = a;

00007FF7BFFC1854 lea rax,[a] //变量a的地址传给rax寄存器中

00007FF7BFFC1858 mov qword ptr [ra],rax //将rax中的地址给ra

ra = 99;

00007FF7BFFC185C mov rax,qword ptr [ra]

00007FF7BFFC1860 mov dword ptr [rax],63h

int* pa = &a;

00007FF7BFFC1866 lea rax,[a] //变量a的地址传给rax寄存器中

00007FF7BFFC186A mov qword ptr [pa],rax //将rax中的地址给pa

*pa = 99;

00007FF7BFFC186E mov rax,qword ptr [pa]

00007FF7BFFC1872 mov dword ptr [rax],63h

汇编指令大致都是相同的,也就是说它和指针实际上是同根同源的。

虽然指针和引用最终在编译中的实现是一样的,但是引用的形式大大方便了使用也更安全。有人说:"引用只是一个别名,不会占内存空间?"通过这个事实我们可以揭穿这个谎言,实际上引用也是占内存空间的。

理解引用小技巧:

C++中与C的区别最明显的是什么?不就是面向对象的特性吗?其实可以把引用当做一个封装了的指针,对对象的操作会被重载成对该指针指向对象的操作。

指针传递和引用传递

指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。

引用传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

引用传递和指针传递是不同的(指针略快于引用),虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针的引用。

不论是指针传递还是引用传递,其效率都远远超过值传递,尤其在处理一些比较大的对象,值传递需要更多的时间开销并且占据更多的内存,因此在传值过程中,尽量使用指针传递和引用传递,而因为相比于指针引用的直观性(指针存在多级指针不方便去理解)和可读性,我们建议能使用引用就使用引用传递,尽量不使用值传递,必须使用指针传递才使用指针。

总而言之,言而总之——它们的这些差别都可以归结为"指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名,引用不改变指向。

文章推荐

相关推荐