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

指针和引用在形式上很好区别,在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的区别最明显的是什么?不就是面向对象的特性吗?其实可以把引用当做一个封装了的指针,对对象的操作会被重载成对该指针指向对象的操作。

指针传递和引用传递

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

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

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

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

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

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭