深入浅出的分析引用有的来龙去脉,进而深入汇编语言探索引用的本质
目录
1、变量名的回顾
变量是一段连续存储空间的别名
程序中通过变量来申请并命名存储空间
通过变量的名字可以使用存储空间
问题:一段连续的存储空间只能有一个别名吗?
2、引用的本质
1、C++中做了一个升级,于是引用就出来了。引用可以看作一个已经定义的变量的别名,系统不会给其分配内存空间
-
// 引用的语法
-
Type& name = var
-
eg:
-
int a = 4 ;
-
int& b = a ; // b为a的别名 引用定义的时候必须进行初始化 类型必须一致
-
b = 5 ; // 操作b就是操作a
-
注:引用在定义的时候必须用同类型的变量(C++为强类型的语言)进行初始化(因为他是一个别名,是谁的别名要指定)
2、引用是一个已经定义的变量的别名,在使用的感受上看,是一个已经存在的存储空间的别名;变量的别名也就是变量的另一个表现形式,通过别名依旧可以操作变量本身。这在某种意义上类似于指针,eg:我们常用的交换函数,见下:
两者的运行结果明显是相同的
3、注:在上例,引用作为函数的形参时,不需要进行初始化,他的初始化发生在函数调用的时候
4、一个引用被声明,则该引用名就只能作为目标变量名的一个别名来使用,所以不能再把该引用名作为其他变量名的别名,任何对该引用的贼值就是对该引用对应的目标变量名的赋值。
5、对引用求地址就是对目标变量求地址。
从输出的结果可以看出,a和b虽然值相同, 但存储地址不同, ba为a的引用, 和a具有同样的值和存储地址
对该引用的贼值就是对该引用对应的目标变量名的赋值。
3、特殊的引用 - const引用
语法:(只有一种)
const Type& name = var
Const引用让变量拥有只读属性,成为只读变量
-
int a = 4;
-
const int& b = a ;
-
int* p = (int*)&b ;
-
b = 5 ; // Error 为只读变量
-
*p = 5 ; // ok 修改变量a的值 通过指针仍然可以修改
在前面的分析中指出,引用的本质为一个变量的别名,不可以使用一个常量来初始化这个引用
但是有一个特殊的引用(const引用)却可以使用常量进行初始化
这时候编译器会为常量值分配空间,并将引用名作为这段空间的别名
4、再谈引用的本质
思考:引用占用存储空间吗?
引用在C++中的内部实现是一个指针常量
注:C++编译器在编译的过程中用指针变量作为引用的内部实现,因此引用所占的空间大小与指针相同
从使用的角度,引用只是一个别名,C++为了实用性而隐藏了引用的存储空间这一个细节
-
struct TRef
-
{
-
char* before;
-
char& ref;
-
char* after;
-
};
-
int main(int argc, char* argv[])
-
{
-
char a = 'a';
-
char& b = a;
-
char c = 'c';
-
TRef r = {&a, b, &c};
-
printf("sizeof(int) = %d\n", sizeof(int));
-
printf("sizeof(r) = %d\n", sizeof(r));
-
printf("sizeof(r.before) = %d\n", sizeof(r.before));
-
printf("sizeof(r.after) = %d\n", sizeof(r.after));
-
printf("&r.before = %p\n", &r.before);
-
printf("&r.after = %p\n", &r.after);
-
return 0;
-
}
运行结果:
分析:在64位系统下,一个指针变量的所占空间的大小为8个字节,从上述结果可以直观的看出引用所占的内存空间大小也为8个字节大小,即指针所占空间的 大小,下面继续通过深入汇编代码进行深入分析,见下:
5、深入汇编分析
第一句:char a = 'a' ; 此句对字符进行了初始化 通过Mov 指令传输数据,0x61就是十进制97,对用a字符的ASCII码
mov指令是数据传送指令,也是最基本的编程指令,用于将一个数据从源地址传送到目标地址(寄存器间的数据传送本质上也是一来样的)。其特点是不破坏源地址单元的内容。
byte ptr 指明了指令访问的内存单元是一个字节单元,ptr是指针pointer
分析 :move BYTE PTR P[ rbp-0x2a],0x61 的意思是:经0x61放到以rbp-0x2a这个地址为其实基地址取连续的一个字节的空间中去
第二句:char& b = a ; 此句进行引用的定义和初始化,b为a变量的另一个别名
分析 :lea rax,[rbp-0x2a] ,的含义为:将地址 rbp-0x2a 处的内容取出来放到rax寄存器里面
LEA 取有效地址指令 (Load Effective Address )
指令格式:LEA 目的,源
指令功能:取源操作数地址的偏移量,并把它传送到目的操作数所在的单元
rax 为一个寄存器
分析 :mov QWORD PTR [rbp-0x28],rax 的含义为:将rax寄存器里面的内容放到基地址为rbp-0x28,连续QWORD大小的内存空间
其中QWORD为8个字节大小(同样验证了64位机器下指针占用8个字节空间的大小)
对汇编代码分析得,引用的内部实现就是指针的操作了,所以说引用必然占用内存空间,并且占用的内存空间是和指针是一模一样的
6、指针变量的引用
格式:类型标识符 *&引用名 = 指针变量名
由上例分析得,从输出的结果可以看出,a和b虽然值相同, 但存储地址不同, p为a的引用, 和a具有同样的值和存储地址
对该引用的贼值就是对该引用对应的目标变量名的赋值。
7、其他注意事项
1、不能建立数组的引用。 因为数组是一个由若干个元素所组成的集合,所以就无法建立一个数组的别名
2、引用是对某一个变量或者目标对象的引用,它本身不是一种数据类型
3、不能建立空指针的引用。例如:
int&φ=NULL;
4、也不能建立空类型void的引用。 例如:void&ra=3·
因为尽管在C++语言中有void数据类型,但没有任何一个变量或常量属于void类型, 所以无法建立其引用。而且引用是对某一目标变量、常量或对象的引用,而不是对某 类型的引用。
5、结构体变量的引用
(1)不能将结构体变量作为→个整体来引用, 只能引用结构体变量中的成员
(2)若结构体的成员本身又是一个结构体变量, 则要使用多个成员运算符一级一级地找到最低一级的成员进行引用
8、引用与函数
1. 作为函数的参数的引用
2. 返回引用的函数
3. 返回引用的函数值作为赋值表达式的左值
一般情况下,赋值表达式的左边只能是变量名, 即被赋值的对象必须是变量。 因为只有变量才能被赋值, 而常量或表达式不能被赋值, 但如果一个函数的返回值是引用时, 赋值号的左边可以是该函数的调用。
程序分析:通过上例可以看出, 返回引用值的函数的调用可以作为赋值号的左值被赋值, 两次函数 调用put( 0)和put( 9 )创作为两条赋值语句的左值, 经过运算后, 这两条赋值语句等价于将数组 vals[ 0]和vals[ 9]赋值为10和20, 因此程序的运行结果为 10和20。
9、引用的意义
C++ 中的引用旨在大多数情况下代替指针(C语言使用指针非常容易出错)
功能性:可以满足大多数需要使用指针的场合;引用的目的主要是在函数参数传递中解决大对象的传递效率和空间不如意的问题。
安全性:可以尽量避开由于指针操作不当而带来的内存错误,在某些方面没有办法避开;用引用传递函数的参数能保证参数传递中不产生副本, 提高传递的效率, 且通过 const的使用, 保证了引用传递的安全性。
操作性:简单易用,功能又强大
引用与指针的区别在于:指针通过某个指针变量指向 一个对象后,对它所指向的变量间接操作, 程序中使用指针使程序的可读性差:而引用本身就是目标变量的别名, 对引用的操作就是对目标变量的操作。
上诉提到在某些方面没有办法避开操作不当而带来的内存错误===》不要返回局部变量的引用
-
int& demo() // 从内部实现看 这里返回的是 int* const
-
{
-
int d = 0;
-
printf("demo: d = %d\n", d);
-
return d; // return &d ; 要返回一个局部变量的地址 ×
-
}
-
int& func()
-
{
-
static int s = 0;
-
printf("func: s = %d\n", s);
-
return s;
-
// return &s ;要返回一个局部变量的地址,可是这个局部变量的引用是静态的,静态的局部变量的存储空间是全局的存储区 √
-
}
-
int main(int argc, char* argv[])
-
{
-
int& rd = demo();
-
int& rs = func();
-
printf("\n");
-
printf("main: rd = %d\n", rd);
-
printf("main: rs = %d\n", rs);
-
printf("\n");
-
rd = 10;
-
rs = 11;
-
demo();
-
func();
-
printf("\n");
-
printf("main: rd = %d\n", rd);
-
printf("main: rs = %d\n", rs);
-
printf("\n");
-
return 0;
-
}
运行结果:
给出一个警告:指出不能返回一个局部的变量d
然而这个变量的警告在运行的时候直接报段错误出来。
小结:引用作为变量别名而存在旨在代替指针
const引用可以使得变量具有只读属性
引用在编译器内部使用指针常量实现
引用的最终本质为指针
引用可以尽可能的避开内存错误,特例为返回局部变量的引用