汇编层探索与讨论c++引用

提出问题

定义一个引用类型和将一个变量转换成引用类型一样吗?
引用比指针安全,真的是这样吗?对引用不理解的话比指针还危险。
为什么要用常量引用传参,只是为了只读?

下面提到词汇前提声明

对象:不是OO里面的对象,而是泛指在c++语言中某种类型(内嵌,结构体,类)的实例,与变量相同的意思
存储体:对象(或变量)的存储内容的控件,一般是内存

下面用到的变量命名规则:
引用:以r开头,紧跟接类型缩写,如float&rf
指针:以p开头,紧跟接类型缩写,如float *pf ,const float * pcf,float * const pfc

c++代码编译成汇编代码后,引用和指针同样是一个 指向内存地址的存储体(一般是内存单元,或优化后使用寄存器,存放指向的内存地址)

第一组:

 uintptr_t uintptr = 0;

所对应的汇编代码是:

00B81832 C7 45 F4 00 00 00 00 mov         dword ptr [uintptr],0  

第二组:

  float flt = 0.f;

所对应的汇编代码是:

00B81839 0F 57 C0             xorps       xmm0,xmm0  
00B8183C F3 0F 11 45 E8       movss       dword ptr [flt],xmm0  

第三组:

 float flt2 = 2.f;

所对应的汇编代码是:

00B81841 F3 0F 10 05 30 7B B8 00 movss       xmm0,dword ptr [__real@40000000 (0B87B30h)]  
00B81849 F3 0F 11 45 DC       movss       dword ptr [flt2],xmm0  

第四组:

float& rf = (float&)uintptr;

所对应的汇编代码是:

00B8184E 8D 45 F4             lea         eax,[uintptr]  
00B81851 89 45 D0             mov         dword ptr [rf],eax  

第五组:

 float* const pfc = &(float&)uintptr;

所对应的汇编代码是:

00B81854 8D 45 F4             lea         eax,[uintptr]  
00B81857 89 45 C4             mov         dword ptr [pfc],eax  

结论:

不同的是它们的访问方式,最根本的区别就是,引用的访问直接访问被引用目标(直接反引用),而指针的访问是访问存放着目标内存地址的存储体,对目标的访问必须显式反引用

也就是说你不能访问到引用存放的目标内存地址的字符串(除非你强行crack)。当你使用赋值符号=对引用进行赋值时,编译器首先将反引用为目标,你只能对引用的目标进行赋值操作。指针在没有使用反引用(*)时,访问的是存放着目标内存地址的存储体,也就是你可以对指针内存单元赋值改变指向,而无法对引用单元赋值改变指向。

第七组:

rf = 1;

所对应的汇编代码是:

004B185A 8B 45 D0             mov         eax,dword ptr [rf]  
004B185D F3 0F 10 05 30 7B 4B 00 movss       xmm0,dword ptr [__real@3f800000 (04B7B30h)]  
004B1865 F3 0F 11 00          movss       dword ptr [eax],xmm0  

同样当你希望通过&符号访问引用内存单元地址时,你可是不可能做到的,因为引用先于地址访问符号,反引用被引用的目标身上,也就是说&访问到的是被引用的目标的地址。以上面的反汇编来说,当你使用&rf访问地址时,rf比&先反引用成引用的目标,&就只能访问到rf引用的目标“dword ptr [rf]”。除非你用汇编代码强行“lea ? ,[rf]”,你才能取得引用的用于存放引用目标的地址的地址。

第八组:

 float* pf = &rf;
0091185F 8B 45 D0             mov         eax,dword ptr [rf]  
00911862 89 45 B8             mov         dword ptr [pf],eax  
 void* pv = (void*)&pfc;
00911865 8D 45 C4             lea         eax,[pfc]  
00911868 89 45 AC             mov         dword ptr [pv],eax  
 void* pv2;
 __asm {
    
    
             lea eax, [rf];
0091186B 8D 45 D0             lea         eax,[rf]  
             mov dword ptr[pv2], eax;
0091186E 89 45 A0             mov         dword ptr [pv2],eax  


如果说引用是被引用目标的别名,那么将一个变量转换成引用类型,是否使用这个变量成为了其它目标的别名。

答案显然不是。那会是什么?先来看看下面这组代码:

第九组:

 rf = 1.;
00425221 8B 45 D0             mov         eax,dword ptr [rf]  
00425224 F3 0F 10 05 30 7B 42 00 movss       xmm0,dword ptr [__real@3f800000 (0427B30h)]  
0042522C F3 0F 11 00          movss       dword ptr [eax],xmm0  
(float&)rf = 2;
00425230 8B 45 D0             mov         eax,dword ptr [rf]  
00425233 F3 0F 10 05 D0 7B 42 00 movss       xmm0,dword ptr [__real@40000000 (0427BD0h)]  
0042523B F3 0F 11 00          movss       dword ptr [eax],xmm0  
(float&)uintptr = 3.;
0042523F F3 0F 10 05 D4 7B 42 00 movss       xmm0,dword ptr [__real@40400000 (0427BD4h)]  
00425247 F3 0F 11 45 F4       movss       dword ptr [uintptr],xmm0  
(float&)uintptr = 3.;
0042524C F3 0F 10 05 D4 7B 42 00 movss       xmm0,dword ptr [__real@40400000 (0427BD4h)]  
00425254 F3 0F 11 45 F4       movss       dword ptr [uintptr],xmm0 
(int&)rf = 2;
00425259 8B 45 D0             mov         eax,dword ptr [rf]  
0042525C C7 00 02 00 00 00    mov         dword ptr [eax],2  
 (int&)uintptr = 3;
00425262 C7 45 F4 03 00 00 00 mov         dword ptr [uintptr],3  

fld是从内存单元加载浮点数到浮点寄存器fstp是将浮点寄存器0的浮点数存储进内存单元

将变量转换成引用类型,不是把变量转换成引用,而是转换成批向变量的引用,按引用的类型来访问

uintptr 是一个非引用类型,(float&)uintptr转换成引用类型,但uintptr不引用其它目标,而是作为一个没有别名的引用目标使用,或者说对uintptr内存单元改变了对它访问的类型(与 * ( float * )&uintptr访问的效果一样)。而(float)uintptr则是按uintptr声明的类型读出它的内存单元数据,然后将数据转换类型。

至于“(float&)rf=2.”,rf这个引用首先反引用(在rf我们不可能通过c++语言访问到的指针单元,放着uintptr变量单元的地址,反引用成uintptr),实质就是对引用目标改变了对它访问的类型,又如“(int &)rf=2;”,并非改变对引用“rf”的访问类型,而是改变对其引用的目标“uintptr”的访问类型。

说到这里,引用的引用是禁止的,这就不难理解了。当你有个名为“rf”的引用,你对这个“rf”的访问(不论读写)都会转化成对引用用目标的访问(限定在高级语言层)。即使你想定义一个引用去引用这个引用的地址也是无功的。“float &rfx=rf;”这句,rfx并不是在引用rf,而是rf直接反引用成了uintptr,实质就是“float& rfx=uintptr;”。如果你要引用到名为rf的引用,你必须要先通过在c++语言层取得名为rf的引用,它存放指针的内存单元地址,而这样是不可能的,所以引用的引用是禁止的。

引用比指针安全????

不知从何起流传引用比指针安全,理由大致如下:

  1. 引用定义之时必须初始化
  2. 引用不会指向null
  3. 引用的指向不可以被修改,只有定义引用的同时才能定义它的指向

1的问题归根到底是程序员不对变量(类构造)进行初始化,这样的程序员不少你还改变不了他们。使用引用,编译器强求他们进行初始化。因为由于没有写初始化造成的问题不只专属于指针。往往有程序员认为花时间写初始化是在浪费时间,这个多变量我还要一个一个初始化,每个类这么多成员变量我还要一个一个初始化,这么多类我还要一个一个为它们写几种构造函数?!!我的工作是按需求写逻辑不是写初始化和构造析构函数

2的问题有点无稽,引用为什么就理所当然不会指向null?

 uintptr_t ptr = 0;
 0046198F C7 45 C4 00 00 00 00 mov         dword ptr [ptr],0 
float flt = 0.;
00461996 0F 57 C0             xorps       xmm0,xmm0  
00461999 F3 0F 11 45 B8       movss       dword ptr [flt],xmm0  
float flt2 = 2.;
0046199E F3 0F 10 05 30 7B 46 00 movss       xmm0,dword ptr [__real@40000000 (0467B30h)]  
004619A6 F3 0F 11 45 AC       movss       dword ptr [flt2],xmm0 
 flt2 = 2.;
004619AB F3 0F 10 05 30 7B 46 00 movss       xmm0,dword ptr [__real@40000000 (0467B30h)]  
004619B3 F3 0F 11 45 AC       movss       dword ptr [flt2],xmm0  
float& rf0 = *(float*)0;
004619B8 C7 45 A0 00 00 00 00 mov         dword ptr [rf0],0  ;反引用到空地址上
float& rf = *new float(flt);
004619BF 6A 04                push        4  
004619C1 E8 4D F7 FF FF       call        operator new (0461113h)  
004619C6 83 C4 04             add         esp,4  
004619C9 89 85 C8 FE FF FF    mov         dword ptr [ebp-138h],eax  
004619CF 83 BD C8 FE FF FF 00 cmp         dword ptr [ebp-138h],0  
004619D6 74 1D                je          main+0C5h (04619F5h)  ;new是否分配
004619D8 8B 85 C8 FE FF FF    mov         eax,dword ptr [ebp-138h]  
004619DE F3 0F 10 45 B8       movss       xmm0,dword ptr [flt]  
004619E3 F3 0F 11 00          movss       dword ptr [eax],xmm0  
004619E7 8B 8D C8 FE FF FF    mov         ecx,dword ptr [ebp-138h]  
004619ED 89 8D C0 FE FF FF    mov         dword ptr [ebp-140h],ecx  
004619F3 EB 0A                jmp         main+0CFh (04619FFh)  
004619F5 C7 85 C0 FE FF FF 00 00 00 00 mov         dword ptr [ebp-140h],0 ;new失败了,名为rf的引用指向null
004619FF 8B 95 C0 FE FF FF    mov         edx,dword ptr [ebp-140h]  
00461A05 89 55 94             mov         dword ptr [rf],edx 

可以看出引用在初始化时,同样会有写情况,使它指向空地址。你并不能阻止别人使用指针反引用来对引用进行初始化。的确在使用指针的时候,我们无时无刻都要为安全考虑多写些指针判断,但是使用了引用,我们就可以堂而皇之认为安全了。

此外使用了引用,还必须要注意它和目标之间的生命周期关系。因为一旦使用了引用,就让“安全”遮眼,不去理会引用目标的生命周期了。因为《effective c++》提到的不要企图返回函数的局部变量的引用或者函数new出来的对象的引用,这两处实质都是引用和引用目标之间的生命周期关系的问题。你引用了一个目标,而这个目标在你使用引用的范围之内就已经被多销毁。你引用了一个目标,而这个目标的生命周期大于你使用引用的范围,你应不应该去管理这个目标的生命周期。《effective c++》提到如果你返回了函数new出来的对象的引用,你不得不自己亲手去delete引用的目标,这样使用起来就很奇怪。从使用引用的角度来看,是不想也不要去理会目标的生命周期的,但是它的本质还是一个指针,指针有的问题它身上也一样会发生。

比如你写了一个模块,模块的类之间存在引用关系,这些引用在你模块内的生命周期关系中一切入好。但某天有人或你要重用其中的某些类,这些类聚合的引用由模块外的类实现代替了,又或者某些类被cache起来,生命周期变大了,引用之间的生命周期关系发生了变化,引用就如指针一样,同样可以执行一处无效的地方。因为你是用的是引用而不是指针,你就可能认为引用不会出问题,因为引用是安全的

3.至于引用不能在定义之外通过语言改变它的指向。当引用同样有可能被定义指向null的时候,它与指针常量就差异不大了。一直以来都认为指针常量不可能像引用那样定义的时候,保证指向非null的对象,但是引用会指向null只是一个谎言。那么指针常量有的问题,引用当然也都有。只是引用的过程中,你的赋值访问都转化为对目标反引用的赋值操作,而对指针常量进行赋值时,编译器会报错指出。但是这样都无法避免有人不去正确去访问你的对象, 例如引用是结构体或类的成员变量。只要某处不正确访问结构体对象或类对象,同样不论是以指针常量还是引用定义的成员变量,一样会被修改。

引用比指针更安全,这只是一个希望,希望通过引用隐藏指针的使用,看起来更加安全。但无论如何引用在汇编层的实现就是指针,让引用代替指针使编程更加安全,只能够是一个谎言,当人人都认为这个谎言是真理的时候就是比使用指针更坏的事情,相信引用比指针更加安全,麻痹对待同一样的事物了。

c++书籍在将引用相关的话题时,更多的是它作为函数的传递参数,与对象传参作比较。你只需要将对象形参改变成引用形参,你就能访问对象的访问对象,还可以避免传参过程临时对象的构造,带来诸多好处。或者在重载类的操作符返回自身的引用,使得类的设计在使用上可以连续书写。

常量引用

说道参数就不得不说一下常量引用,就是引用一个常量了。

首先你不可以用一个非常量的引用,定义它指向一个常量,你必须用一个常量类型的引用,定义它指向一个常量。那么指向常量会发生什么事情呢?请看下面代码:

const float& rcf = 1.f;
00D81A08 F3 0F 10 05 30 7B D8 00 movss       xmm0,dword ptr [__real@3f800000 (0D87B30h)]  
00D81A10 F3 0F 11 85 7C FF FF FF movss       dword ptr [ebp-84h],xmm0  
00D81A18 8D 85 7C FF FF FF    lea         eax,[ebp-84h]  
00D81A1E 89 45 88             mov         dword ptr [rcf],eax  
const float& rcf2 = 1.f;
00D81A21 F3 0F 10 05 30 7B D8 00 movss       xmm0,dword ptr [__real@3f800000 (0D87B30h)]  
00D81A29 F3 0F 11 85 64 FF FF FF movss       dword ptr [ebp-9Ch],xmm0  
00D81A31 8D 85 64 FF FF FF    lea         eax,[ebp-9Ch]  
00D81A37 89 85 70 FF FF FF    mov         dword ptr [rcf2],eax  

可以看到编译器悄悄为常量分配了局部空间,形式就像

const float implict=1.f;//dword ptr[ebp-84h]
const float &rcf=implict;
const float implict2=1.f;//dword ptr[ebp-9Ch]
const float &rcf2=implict2;

为什么不打算对其修改器内容的引用形参,使用常量引用呢?让其只读是其一,其二是你不能将一个常量去定义一个引用形参。

void foo(float&);
void foo2(const float&);

main()
{
    
    
foo(1.f);//error
foo2(1.f);//ok
}

在这里插入图片描述
在这里插入图片描述

那么这个foo2函数是如何接受一个常量,去定义一个常量引用形参呢?请看反汇编代码:

        foo2(1.f);//ok
003519B8 F3 0F 10 05 30 7B 35 00 movss       xmm0,dword ptr [__real@3f800000 (0357B30h)]  
003519C0 F3 0F 11 85 98 FE FF FF movss       dword ptr [ebp-168h],xmm0  
003519C8 8D 85 98 FE FF FF    lea         eax,[ebp-168h]  
003519CE 50                   push        eax  
003519CF E8 F4 F6 FF FF       call        foo2 (03510C8h)  
003519D4 83 C4 04             add         esp,4  

同样,编译器偷偷为这个常量开辟了一个临时空间,将其赋值为1.f常量,并定义foo2的形参指向这个没有名字的临时变量。

另外对于临时类对象,赋值给其它变量,则会拷贝这个临时类对象,并随后析构这个临时类对象。但是如果将一个临时类对象去定义一个引用呢?

B();
00401163 lea ecx,[ebp-59h] 
00401166 call B::B (0401019h) 
0040116B lea ecx,[ebp-59h] 
0040116E call B::~B (0401028h) 
 A& ra = A();
00401173 lea ecx,[ebp-21h] 
00401176 call A::A (040101Eh) 
0040117B lea edx,[ebp-21h] 
0040117E mov dword ptr [ra],edx 
 C* const pcc = &C();
00401181 lea ecx,[ebp-5Ah] 
00401184 call C::C (0401037h) 
00401189 mov dword ptr [pcc],eax 
0040118C lea ecx,[ebp-5Ah] 
0040118F call C::~C (040103Ch) 
 ...
 return 0;
004011C7 mov dword ptr [ebp-64h],0 
004011CE lea ecx,[ebp-21h] 
004011D1 call A::~A (0401023h) 
004011D6 mov eax,dword ptr [ebp-64h] 
}
004011D9 mov esp,ebp 
004011DB pop ebp 
004011DC ret 

这个局部的临时对象“A()”生命周期延长到了局部范围的结束,因为编译器要保证这个引用不会产生一个迷途引用。而使用指针常量则没有办法延长一个临时对象的生命周期,“C()”这个临时对象在定义了指针常量pcc的指向后,就马上析构了,留下了一个迷途指针。

相同的讨论可以看 stackoverflow
《Why is a c++ reference considered safer than a pointer?》

结合上面的内容,你能说出下面的指向和发生了什么操作吗?

uintptr_t uintptr = 0;
float flt = 0.f;
float flt2 = 2.f;
float& rf = (float&)uintptr;
float& rf2 = (float&)uintptr = flt;
float& rf3 = (float&)rf2 = flt2;
float*& rpf2 = (float*&)uintptr = &flt;

你说对了吗,请参看汇编代码:

 uintptr_t uintptr = 0;
003F5022 C7 45 F4 00 00 00 00 mov         dword ptr [uintptr],0  
float flt = 0.f;
003F5029 0F 57 C0             xorps       xmm0,xmm0  
003F502C F3 0F 11 45 E8       movss       dword ptr [flt],xmm0  
 float flt2 = 2.f;
003F5031 F3 0F 10 05 30 7B 3F 00 movss       xmm0,dword ptr [__real@3f800000 (03F7B30h)]  
003F5039 F3 0F 11 45 DC       movss       dword ptr [flt2],xmm0  
 float& rf = (float&)uintptr;
003F503E 8D 45 F4             lea         eax,[uintptr]  
003F5041 89 45 D0             mov         dword ptr [rf],eax  
float& rf2 = (float&)uintptr = flt;
003F5044 F3 0F 10 45 E8       movss       xmm0,dword ptr [flt]  
003F5049 F3 0F 11 45 F4       movss       dword ptr [uintptr],xmm0  
003F504E 8D 45 F4             lea         eax,[uintptr]  
003F5051 89 45 C4             mov         dword ptr [rf2],eax 
float& rf3 = (float&)rf2 = flt2;
003F5054 8B 45 C4             mov         eax,dword ptr [rf2]  
003F5057 F3 0F 10 45 DC       movss       xmm0,dword ptr [flt2]  
003F505C F3 0F 11 00          movss       dword ptr [eax],xmm0  
003F5060 8B 4D C4             mov         ecx,dword ptr [rf2]  
003F5063 89 4D B8             mov         dword ptr [rf3],ecx  
float*& rpf2 = (float*&)uintptr = &flt;
003F5066 8D 45 E8             lea         eax,[flt]  
003F5069 89 45 F4             mov         dword ptr [uintptr],eax  
003F506C 8D 4D F4             lea         ecx,[uintptr]  
003F506F 89 4D AC             mov         dword ptr [rpf2],ecx  

猜你喜欢

转载自blog.csdn.net/CSNN2019/article/details/114230479
今日推荐