2.5 地址、指针和引用
在C++中,地址标号使用十六进制表示,取一个变量的地址使用“&”符号,只有变量才存在内存地址,常量(见2.6节)没有地址(不包括const定义的伪常量)。例如,对于数字100,我们无法取出它的地址。取出的地址是一个常量值,无法再对其取地址了。
指针的定义使用“TYPE*”格式,TYPE为数据类型,任何数据类型都可以定义指针。指针本身也是一种数据类型,用于保存各种数据类型在内存中的地址。指针变量同样可以取出地址,所以会出现多级指针。
引用的定义格式为“TYPE&”,TYPE为数据类型。在C++中是不可以单独定义的,并且在定义时就要进行初始化。引用表示一个变量的别名,对它的任何操作本质上都是在操作它所表示的变量。详细讲解见2.5.3节。
2.5.1 指针和地址的区别
在32位应用程序中,地址是一个由32位二进制数字组成的值;在64位应用程序中,地址是一个由64位二进制数字组成的值。为了便于查看,转换成十六进制数字显示出来,用于标识内存编号。指针是用于保存这个编号的一种变量类型,它包含在内存中,所以可以取出指针类型变量在内存中的位置——地址。由于指针保存的数据都是地址,所以无论什么类型的指针,32位程序都占据4字节的内存空间,64位程序都占据8字节的内存空间,如图2-9所示。
图2-9 地址和指针
指针可以根据指针类型对地址对应的数据进行解释。而一个地址值无法单独解释数据,对于图2-9中0x0135FE04这个地址值,仅凭借它本身无法说明该地址处对应数据的信息。如果是在一个int类型的指针中保存这个地址,就可以将其看作int类型数据的起始地址,向后数4字节到0x0135FE08处,将0x0135FE04~0x0135FE08中的数据按整型存储方式解释,详见2.5.2节。
指针和地址之间的不同点如表2-3所示。
表2-3 指针和地址之间的不同点
指针和地址之间的共同点如表2-4所示。
表2-4 指针和地址之间的共同点
2.5.2 各类型指针的工作方式
在C++中,任何数据类型都有对应的指针类型。我们从前面的学习中了解到,指针保存的都是地址,为什么还需要类型作为修饰呢?因为我们需要用类型去解释这个地址中的数据。每种数据类型所占的内存空间不同,指针只保存了存放数据的首地址,而没有指明该在哪里结束。这时就需要根据对应的类型来寻找解释数据的结束地址。同一地址使用不同类型的指针进行访问,取出的内容就会不一样,如代码清单2-4所示。
代码清单2-4 不同类型指针访问同一地址
// C++源码 #include <stdio.h> int main(int argc, char* argv[]) { int n = 0x12345678; int *p1 = &n; char *p2 = (char*)&n; short *p3 = (short*)&n; printf("%08x \r\n", *p1); printf("%08x \r\n", *p2); printf("%08x \r\n", *p3); return 0; } //x86_vs对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 sub esp, 10h ;申请局部变量 00401006 mov dword ptr [ebp-4], 12345678h ;n=0x12345678 0040100D lea eax, [ebp-4] 00401010 mov [ebp-8], eax ;p1=&n 00401013 lea ecx, [ebp-4] 00401016 mov [ebp-0Ch], ecx ;p2=&n 00401019 lea edx, [ebp-4] 0040101C mov [ebp-10h], edx ;p3=&n 0040101F mov eax, [ebp-8] ;取出地址 00401022 mov ecx, [eax] ;取4字节内容 00401024 push ecx ;参数2 *p1 00401025 push offset a08x ;参数1 "%08x \r\n" 0040102A call sub_4010A0 ;调用printf函数 0040102F add esp, 8 ;平衡栈 00401032 mov edx, [ebp-0Ch] ;取出地址 00401035 movsx eax, byte ptr [edx] ;取1字节内容,高位符号扩展成4字节 00401038 push eax ;参数2 *p2 00401039 push offset a08x_0 ;参数1 "%08x \r\n" 0040103E call sub_4010A0 ;调用printf函数 00401043 add esp, 8 ;平衡栈 00401046 mov ecx, [ebp-10h] ;取出地址 00401049 movsx edx, word ptr [ecx] ;取2字节内容,高位符号扩展成4字节 0040104C push edx ;参数2 *p3 0040104D push offset a08x_1 ;参数1 "%08x \r\n" 00401052 call sub_4010A0 ;调用printf函数 00401057 add esp, 8 ;平衡栈 0040105A xor eax, eax 0040105C mov esp, ebp 0040105E pop ebp 0040105F retn //x86_gcc对应汇编代码讲解 00401510 push ebp 00401511 mov ebp, esp 00401513 and esp, 0FFFFFFF0h ;对齐栈 00401516 sub esp, 20h 00401519 call ___main ;调用初始化函数 0040151E mov dword ptr [esp+10h], 12345678h;n=0x12345678 00401526 lea eax, [esp+10h] 0040152A mov [esp+1Ch], eax ;p1=&n 0040152E lea eax, [esp+10h] 00401532 mov [esp+18h], eax ;p2=&n 00401536 lea eax, [esp+10h] 0040153A mov [esp+14h], eax ;p3=&n 0040153E mov eax, [esp+1Ch] ;取出地址 00401542 mov eax, [eax] ;取4字节内容 00401544 mov [esp+4], eax ;参数2 *p1 00401548 mov dword ptr [esp], offset a08x ;参数1 "%08x \r\n" 0040154F call _printf ;调用printf函数 00401554 mov eax, [esp+18h] ;取出地址 00401558 movzx eax, byte ptr [eax] ;取1字节内容 0040155B movsx eax, al ;高位符号扩展成4字节 0040155E mov [esp+4], eax ;参数2 *p2 00401562 mov dword ptr [esp], offset a08x ;参数1 "%08x \r\n" 00401569 call _printf ;调用printf函数 0040156E mov eax, [esp+14h] ;取出地址 00401572 movzx eax, word ptr [eax] ;取2字节内容,高位0扩展成4字节 00401575 cwde ;高位符号扩展成4字节 00401576 mov [esp+4], eax ;参数2 *p3 0040157A mov dword ptr [esp], offset a08x ;参数1 "%08x \r\n" 00401581 call _printf ;调用printf函数 00401586 mov eax, 0 0040158B leave 0040158C retn //x86_clang对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 push esi 00401004 sub esp, 30h 00401007 mov eax, [ebp+0Ch] 0040100A mov ecx, [ebp+8] 0040100D mov dword ptr [ebp-8], 0 00401014 mov dword ptr [ebp-0Ch], 12345678h;n=0x12345678 0040101B lea edx, [ebp-0Ch] 0040101E mov [ebp-10h], edx ;p1=&n 00401021 mov esi, edx 00401023 mov [ebp-14h], esi ;p2=&n 00401026 mov [ebp-18h], edx ;p3=&n 00401029 mov edx, [ebp-10h] ;取出地址 0040102C mov edx, [edx] ;取4字节内容 0040102E lea esi, a08x 00401034 mov [esp], esi ;参数1 "%08x \r\n" 00401037 mov [esp+4], edx ;参数2 *p1 0040103B mov [ebp-1Ch], eax 0040103E mov [ebp-20h], ecx 00401041 call sub_401090 ;调用printf函数 00401046 mov ecx, [ebp-14h] ;取出地址 00401049 movsx ecx, byte ptr [ecx] ;取1字节内容,高位符号扩展成4字节 0040104C lea edx, a08x 00401052 mov [esp], edx ;参数1 "%08x \r\n" 00401055 mov [esp+4], ecx ;参数2 *p2 00401059 mov [ebp-24h], eax 0040105C call sub_401090 ;调用printf函数 00401061 mov ecx, [ebp-18h] ;取出地址 00401064 movsx ecx, word ptr [ecx] ;取出2字节的内容,高位符号扩展成4字节 00401067 lea edx, a08x 0040106D mov [esp], edx; ;参数1 "%08x \r\n" 00401070 mov [esp+4], ecx ;参数2 *p3 00401074 mov [ebp-28h], eax 00401077 call sub_401090 ;调用printf函数 0040107C xor ecx, ecx 0040107E mov [ebp-2Ch], eax 00401081 mov eax, ecx 00401083 add esp, 30h 00401086 pop esi 00401087 pop ebp 00401088 retn //x64_vs对应汇编代码讲解 0000000140001000 mov [rsp+10h], rdx 0000000140001005 mov [rsp+8], ecx 0000000140001009 sub rsp, 48h 000000014000100D mov dword ptr [rsp+20h], 12345678h; n=0x12345678 0000000140001015 lea rax, [rsp+20h] 000000014000101A mov [rsp+28h], rax ;p1=&n 000000014000101F lea rax, [rsp+20h] 0000000140001024 mov [rsp+30h], rax ;p2=&n 0000000140001029 lea rax, [rsp+20h] 000000014000102E mov [rsp+38h], rax ;p3=&n 0000000140001033 mov rax, [rsp+28h] ;取出地址 0000000140001038 mov edx, [rax] ;参数2取出4字节内容 *p1 000000014000103A lea rcx, a08x ;参数1 "%08x \r\n" 0000000140001041 call sub_1400010E0 ;调用printf函数 0000000140001046 mov rax, [rsp+30h] ;取出地址 000000014000104B movsx eax, byte ptr [rax] ;取出1字节的内容,高位符号扩展成4字节 000000014000104E mov edx, eax ;参数2 *p2 0000000140001050 lea rcx, a08x_0 ;参数1 "%08x \r\n" 0000000140001057 call sub_1400010E0 ;调用printf函数 000000014000105C mov rax, [rsp+38h] ;取出地址 0000000140001061 movsx eax, word ptr [rax] ;取出2字节内容,高位符号扩展成4字节 0000000140001064 mov edx, eax ;参数2 *p3 0000000140001066 lea rcx, a08x_1 ;参数1 "%08x \r\n" 000000014000106D call sub_1400010E0 ;调用printf函数 0000000140001072 xor eax, eax 0000000140001074 add rsp, 48h 0000000140001078 retn //x64_gcc对应汇编代码讲解 0000000000401550 push rbp 0000000000401551 mov rbp, rsp 0000000000401554 sub rsp, 40h 0000000000401558 mov [rbp+10h], ecx 000000000040155B mov [rbp+18h], rdx 000000000040155F call __main ;调用初始化函数 0000000000401564 mov dword ptr [rbp-1Ch], 12345678h;n=0x12345678 000000000040156B lea rax, [rbp-1Ch] 000000000040156F mov [rbp-8], rax ;p1=&n 0000000000401573 lea rax, [rbp-1Ch] 0000000000401577 mov [rbp-10h], rax ;p2=&n 000000000040157B lea rax, [rbp-1Ch] 000000000040157F mov [rbp-18h], rax ;p3=&n 0000000000401583 mov rax, [rbp-8] ;取出地址 0000000000401587 mov eax, [rax] ;取出4字节内容 0000000000401589 mov edx, eax ;参数2 *p1 000000000040158B lea rcx, Format ;参数1 "%08x \r\n" 0000000000401592 call printf ;调用printf函数 0000000000401597 mov rax, [rbp-10h] ;取出地址 000000000040159B movzx eax, byte ptr [rax] ;取出1字节内容,高位0扩展成4字节 000000000040159E movsx eax, al ;高位符号扩展成4字节 00000000004015A1 mov edx, eax ;参数2 *p2 00000000004015A3 lea rcx, Format ;参数1 "%08x \r\n" 00000000004015AA call printf ;调用printf函数 00000000004015AF mov rax, [rbp-18h] ;取出地址 00000000004015B3 movzx eax, word ptr [rax] ;取出2字节内容,高位0扩展成4字节 00000000004015B6 cwde ;高位符号扩展成4字节 00000000004015B7 mov edx, eax ;参数2 *p3 00000000004015B9 lea rcx, Format ;参数1 "%08x \r\n" 00000000004015C0 call printf ;调用printf函数 00000000004015C5 mov eax, 0 00000000004015CA add rsp, 40h 00000000004015CE pop rbp 00000000004015CF retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 68h 0000000140001004 mov dword ptr [rsp+64h], 0 000000014000100C mov [rsp+58h], rdx 0000000140001011 mov [rsp+54h], ecx 0000000140001015 mov dword ptr [rsp+50h], 12345678h; n=0x12345678 000000014000101D lea rdx, [rsp+50h] 0000000140001022 mov [rsp+48h], rdx ;p1=&n 0000000140001027 mov rax, rdx 000000014000102A mov [rsp+40h], rax ;p2=&n 000000014000102F mov [rsp+38h], rdx ;p3=&n 0000000140001034 mov rax, [rsp+48h] ;取出地址 0000000140001039 mov edx, [rax] ;参数2取出4字节内容 *p1 000000014000103B lea rcx, a08x ;参数1 "%08x \r\n" 0000000140001042 call sub_140001090 ;调用printf函数 0000000140001047 mov rcx, [rsp+40h] ;取出地址 000000014000104C movsx edx, byte ptr [rcx];参数2取出1字节内容,高位符号扩展成4字节 000000014000104F lea rcx, a08x ;参数1 "%08x \r\n" 0000000140001056 mov [rsp+34h], eax 000000014000105A call sub_140001090 ;调用printf函数 000000014000105F mov rcx, [rsp+38h] ;取出地址 0000000140001064 movsx edx, word ptr [rcx] ;参数2取出2字节内容,高位符号扩展成4字节 0000000140001067 lea rcx, a08x ;参数1 "%08x \r\n" 000000014000106E mov [rsp+30h], eax 0000000140001072 call sub_140001090 ;调用printf函数 0000000140001077 xor edx, edx 0000000140001079 mov [rsp+2Ch], eax 000000014000107D mov eax, edx 000000014000107F add rsp, 68h 0000000140001083 retn
代码清单2-4中使用了3种方式对变量n的地址进行解释。变量n在内存中的数据为“78 56 34 12”,首地址从“78”开始。指针p1为int类型,以int类型在内存中占用的空间大小和排列方式对地址进行解释,然后取出数据。int类型占4字节内存空间,以小尾方式排列,取出内容为“12345678”,是一个十六进制的数字。同理,p2、p23将会按照它们的指针类型对地址数据进行解释。指针取内容的操作分为两个步骤:先取出指针中保存的地址信息,然后针对这个地址取内容,这是一个间接寻址的过程,也是识别指针的重要依据。该示例运行结果如图2-10所示。
图2-10 各类型指针解释地址的结果
通过代码清单2-4中指针取内容的过程可得出结论,不同类型的指针对地址的解释都取自其自身指针类型。
指针都支持哪些运算符号呢?在C++中,所有指针类型都只支持加法和减法。指针是用来保存数据地址、解释地址的,因此只有加法与减法才有意义,其他运算对于指针而言没有任何意义。
指针加法用于地址偏移,但并不像数学中的加法那样简单。指针加1后,指针内保存的地址值并不一定会加1,运算结果取决于指针类型,如指针类型为int,地址值将会加4,这个4是根据类型大小所得到的值。C++为什么要用这种烦琐的地址偏移方法呢?因为当指针中保存的地址为数组首地址时,为了能够利用指针加1后访问到数组内下一成员,所以加的是类型长度,而非数字1,如代码清单2-5所示。
代码清单2-5 各类型指针的寻址方式
// C++ 源码 #include <stdio.h> int main(int argc, char* argv[]) { char ary[5] = {(char)0x01, (char)0x23, (char)0x45, (char)0x67, (char)0x89}; int *p1 = (int*)ary; char *p2 = (char*)ary; short *p3 = (short*)ary; p1 += 1; p2 += 1; p3 += 1; return 0; } //x86_vs对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 sub esp, 18h 00401006 mov eax, ___security_cookie 0040100B xor eax, ebp 0040100D mov [ebp-4], eax ;缓冲区溢出检查代码 00401010 mov byte ptr [ebp-0Ch], 1 ;ary[0]=1 00401014 mov byte ptr [ebp-0Bh], 23h ;ary[1]=0x23 00401018 mov byte ptr [ebp-0Ah], 45h ;ary[2]=0x45 0040101C mov byte ptr [ebp-9], 67h ;ary[3]=0x67 00401020 mov byte ptr [ebp-8], 89h ;ary[4]=0x89 00401024 lea eax, [ebp-0Ch] 00401027 mov [ebp-10h], eax ;p1=(int*)ary 0040102A lea ecx, [ebp-0Ch] 0040102D mov [ebp-14h], ecx ;p2=(char*)ary 00401030 lea edx, [ebp-0Ch] 00401033 mov [ebp-18h], edx ;p3=(short*)ary 00401036 mov eax, [ebp-10h] 00401039 add eax, 4 0040103C mov [ebp-10h], eax ;p1 += 1 0040103F mov ecx, [ebp-14h] 00401042 add ecx, 1 00401045 mov [ebp-14h], ecx ;p2 += 1 00401048 mov edx, [ebp-18h] 0040104B add edx, 2 0040104E mov [ebp-18h], edx ;p3 += 1 00401051 xor eax, eax 00401053 mov ecx, [ebp-4] 00401056 xor ecx, ebp 00401058 call @__security_check_cookie@4 ;缓冲区溢出检查代码 0040105D mov esp, ebp 0040105F pop ebp 00401060 retn //x86_gcc对应汇编代码讲解 00401510 push ebp 00401511 mov ebp, esp 00401513 and esp, 0FFFFFFF0h ;对齐栈 00401516 sub esp, 20h 00401519 call ___main ;调用初始化函数 0040151E mov byte ptr [esp+0Fh], 1 ;ary[0]=1 00401523 mov byte ptr [esp+10h], 23h ;ary[1]=0x23 00401528 mov byte ptr [esp+11h], 45h ;ary[2]=0x45 0040152D mov byte ptr [esp+12h], 67h ;ary[3]=0x67 00401532 mov byte ptr [esp+13h], 89h ;ary[4]=0x89 00401537 lea eax, [esp+0Fh] 0040153B mov [esp+1Ch], eax ;p1 = (int*)ary 0040153F lea eax, [esp+0Fh] 00401543 mov [esp+18h], eax ;p2 = (char*)ary 00401547 lea eax, [esp+0Fh] 0040154B mov [esp+14h], eax ;p3 = (short*)ary 0040154F add dword ptr [esp+1Ch], 4 ;p1 += 1 00401554 add dword ptr [esp+18h], 1 ;p2 += 1 00401559 add dword ptr [esp+14h], 2 ;p3 += 1 0040155E mov eax, 0 00401563 leave 00401564 retn //x86_clang对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 push ebx 00401004 push edi 00401005 push esi 00401006 sub esp, 20h 00401009 mov eax, [ebp+0Ch] 0040100C mov ecx, [ebp+8] 0040100F xor edx, edx 00401011 lea esi, [ebp-15h] 00401014 mov dword ptr [ebp-10h], 0 0040101B mov edi, ds:dword_40D150 00401021 mov [ebp-15h], edi ;ary[]= {1, 0x23, 0x45, 0x67} 00401024 mov bl, ds:byte_40D154 0040102A mov [ebp-11h], bl ;ary[4] = 0x89 0040102D mov edi, esi 0040102F mov [ebp-1Ch], edi ;p1 = (int*)ary 00401032 mov [ebp-20h], esi ;p2 = (char*)ary 00401035 mov [ebp-24h], esi ;p3 = (short)*ary 00401038 mov esi, [ebp-1Ch] 0040103B add esi, 4 0040103E mov [ebp-1Ch], esi ;p1 += 4; 00401041 mov esi, [ebp-20h] 00401044 add esi, 1 00401047 mov [ebp-20h], esi ;p2 += 1; 0040104A mov esi, [ebp-24h] 0040104D add esi, 2 00401050 mov [ebp-24h], esi ;p3 += 2; 00401053 mov [ebp-28h], eax 00401056 mov eax, edx 00401058 mov [ebp-2Ch], ecx 0040105B add esp, 20h 0040105E pop esi 0040105F pop edi 00401060 pop ebx 00401061 pop ebp 00401062 retn //x64_vs对应汇编代码讲解 0000000140001000 mov [rsp+10h], rdx ;保存argv到预留栈空间 0000000140001005 mov [rsp+8], ecx ;保存argc到预留栈空间 0000000140001009 sub rsp, 38h 000000014000100D mov rax, cs:__security_cookie 0000000140001014 xor rax, rsp 0000000140001017 mov [rsp+20h], rax ;缓冲区溢出检查代码 000000014000101C mov byte ptr [rsp+18h], 1 ;ary[0] = 1 0000000140001021 mov byte ptr [rsp+19h], 23h ;ary[1] = 0x23 0000000140001026 mov byte ptr [rsp+1Ah], 45h ;ary[2] = 0x45 000000014000102B mov byte ptr [rsp+1Bh], 67h ;ary[3] = 0x67 0000000140001030 mov byte ptr [rsp+1Ch], 89h ;ary[4] = 0x89 0000000140001035 lea rax, [rsp+18h] 000000014000103A mov [rsp], rax ;p1 = (int*)ary 000000014000103E lea rax, [rsp+18h] 0000000140001043 mov [rsp+8], rax ;p2 = (char*)ary 0000000140001048 lea rax, [rsp+18h] 000000014000104D mov [rsp+10h], rax ;p3 = (short*)ary 0000000140001052 mov rax, [rsp] 0000000140001056 add rax, 4 000000014000105A mov [rsp], rax ;p1 += 1 000000014000105E mov rax, [rsp+8] 0000000140001063 inc rax 0000000140001066 mov [rsp+8], rax ;p2 += 1 000000014000106B mov rax, [rsp+10h] 0000000140001070 add rax, 2 0000000140001074 mov [rsp+10h], rax ;p3 += 1 0000000140001079 xor eax, eax 000000014000107B mov rcx, [rsp+20h] 0000000140001080 xor rcx, rsp ; StackCookie 0000000140001083 call __security_check_cookie ;缓冲区溢出检查代码 0000000140001088 add rsp, 38h 000000014000108C retn //x64_gcc对应汇编代码讲解 0000000000401550 push rbp 0000000000401551 mov rbp, rsp 0000000000401554 sub rsp, 40h 0000000000401558 mov [rbp+10h], ecx 000000000040155B mov [rbp+18h], rdx 000000000040155F call __main ;调用初始化函数 0000000000401564 mov byte ptr [rbp-1Dh], 1 ;ary[0] = 1 0000000000401568 mov byte ptr [rbp-1Ch], 23h ;ary[1] = 0x23 000000000040156C mov byte ptr [rbp-1Bh], 45h ;ary[2] = 0x45 0000000000401570 mov byte ptr [rbp-1Ah], 67h ;ary[3] = 0x67 0000000000401574 mov byte ptr [rbp-19h], 89h ;ary[4] = 0x89 0000000000401578 lea rax, [rbp-1Dh] 000000000040157C mov [rbp-8], rax ;p1 = (int*)ary 0000000000401580 lea rax, [rbp-1Dh] 0000000000401584 mov [rbp-10h], rax ;p2 = (char*)ary 0000000000401588 lea rax, [rbp-1Dh] 000000000040158C mov [rbp-18h], rax ;p3 = (short*)ary 0000000000401590 add qword ptr [rbp-8], 4 ;p1 += 1 0000000000401595 add qword ptr [rbp-10h], 1 ;p2 += 1 000000000040159A add qword ptr [rbp-18h], 2 ;p3 += 1 000000000040159F mov eax, 0 00000000004015A4 add rsp, 40h 00000000004015A8 pop rbp 00000000004015A9 retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 38h 0000000140001004 xor eax, eax 0000000140001006 lea r8, [rsp+1Fh] 000000014000100B mov dword ptr [rsp+34h], 0 0000000140001013 mov [rsp+28h], rdx 0000000140001018 mov [rsp+24h], ecx 000000014000101C mov ecx, cs:dword_14000D2C0 0000000140001022 mov [rsp+1Fh], ecx ;ary[]= {1, 0x23, 0x45, 0x67} 0000000140001026 mov r9b, cs:byte_14000D2C4 000000014000102D mov [rsp+23h], r9b ;ary[4] = 0x89 0000000140001032 mov rdx, r8 0000000140001035 mov [rsp+10h], rdx ;p1 = (int*)ary 000000014000103A mov [rsp+8], r8 ;p2 = (char*)ary 000000014000103F mov [rsp], r8 ;p3 = (short*)ary 0000000140001043 mov rdx, [rsp+10h] 0000000140001048 add rdx, 4 000000014000104C mov [rsp+10h], rdx ;p1 += 1 0000000140001051 mov rdx, [rsp+8] 0000000140001056 add rdx, 1 000000014000105A mov [rsp+8], rdx ;p2 += 1 000000014000105F mov rdx, [rsp] 0000000140001063 add rdx, 2 0000000140001067 mov [rsp], rdx ;p3 += 1 000000014000106B add rsp, 38h 000000014000106F ret
代码清单2-5演示了对不同类型指针进行加1偏移得到的结果。它们偏移后的地址都是由指针类型决定的,以指针保存的地址作为寻址[首地址],加上[偏移量],最终得到[目标地址]。偏移量的计算方式为指针类型长度乘以移动次数,因此得出指针寻址公式如下所示。
type *p; // 这里用 type 泛指某类型的指针 // 省略指针赋值代码 p+n 的目标地址 = 首地址 + sizeof( 指针类型 type) * n
对于偏移量为负数的情况,此公式同样适用。套用公式,得到的地址值会小于首地址,这时指针是在向后寻址。所以指针可以做减法操作,但乘法与除法对于指针寻址而言是没有意义的。两指针做减法操作是在计算两个地址之间的元素个数,结果为有符号整数,进行减法操作的两指针必须是同类指针。可用于两指针中的地址比较,也可用于其他场合,比如求数组元素个数,其计算公式如下所示。
type *p, *q; // 这里用type泛指某类型的指针 // 省略指针赋值代码 p-q = ((int)p - (int)q) / sizeof(指针类型type)
另外,两指针相加也是没有意义的。将指针访问公式与指针寻址公式结合后,可针对所有类型的指针进行操作。在实际运用中要灵活使用,同时也要谨慎操作,以免将指针指向意料之外的地址,错误地修改地址中的数据,造成程序崩溃。
在代码清单2-5中,指针p1加1后取出的内容就是数组ary以外的数据。ary数组只有5字节数据长度,而p1将访问到数组的第4项,对其取内容后得到的数据是以ary数组的第4项为起始地址的4字节数据。分析出的结果如图2-11所示。
图2-11 字符数组ary的内存信息
2.5.3 引用
引用类型在C++中被描述为变量的别名。C++为了简化操作,对指针的操作进行了封装,产生了引用类型。引用类型实际上就是指针类型,只不过用于存放地址的内存空间对使用者而言是隐藏的。下面通过示例来揭开这个谜底,如代码清单2-6所示。
代码清单2-6 引用类型揭秘
// C++ 源码 #include <stdio.h> void add(int &ref){ ref++; } int main(int argc, char* argv[]) { int n = 0x12345678; int &ref = n; add(ref); return 0; } //x86_vs对应汇编代码讲解 00401020 push ebp 00401021 mov ebp, esp 00401023 sub esp, 8 00401026 mov dword ptr [ebp-4], 12345678h;n=0x12345678 0040102D lea eax, [ebp-4] ;eax=&n 00401030 mov [ebp-8], eax ;[ebp-8]存放n的地址,int& ref = n 00401033 mov ecx, [ebp-8] 00401036 push ecx ;参数1传递n的地址 00401037 call sub_401000 ;调用add函数 0040103C add esp, 4 ;平衡栈 0040103F xor eax, eax 00401041 mov esp, ebp 00401043 pop ebp 00401044 retn //x86_gcc对应汇编代码讲解 00401523 push ebp 00401524 mov ebp, esp 00401526 and esp, 0FFFFFFF0h ;对齐栈 00401529 sub esp, 20h 0040152C call ___main ;调用初始化函数 00401531 mov dword ptr [esp+18h], 12345678h;n=0x12345678 00401539 lea eax, [esp+18h] ;eax=&n 0040153D mov [esp+1Ch], eax ;[esp+1Ch]存放n的地址,int& ref = n 00401541 mov eax, [esp+1Ch] 00401545 mov [esp], eax ;参数1传递n的地址int * 00401548 call __Z3addRi ;调用函数add(int &) 0040154D mov eax, 0 00401552 leave 00401553 retn //x86_clang对应汇编代码讲解 00401020 push ebp 00401021 mov ebp, esp 00401023 sub esp, 18h 00401026 mov eax, [ebp+0Ch] 00401029 mov ecx, [ebp+8] 0040102C mov dword ptr [ebp-4], 0 00401033 mov dword ptr [ebp-8], 12345678h;n=0x12345678 0040103A lea edx, [ebp-8] ;edx=&n 0040103D mov [ebp-0Ch], edx ;[ebp-0Ch]存放n的地址,int& ref = n 00401040 mov edx, [ebp-0Ch] 00401043 mov [esp], edx ;参数1传递n的地址 00401046 mov [ebp-10h], eax 00401049 mov [ebp-14h], ecx 0040104C call sub_401000 ;调用add函数 00401051 xor eax, eax 00401053 add esp, 18h 00401056 pop ebp 00401057 retn //x64_vs对应汇编代码讲解 0000000140001020 mov [rsp+10h], rdx 0000000140001025 mov [rsp+8], ecx 0000000140001029 sub rsp, 38h 000000014000102D mov dword ptr [rsp+20h], 12345678h;n=0x12345678 0000000140001035 lea rax, [rsp+20h] ;rax=&n 000000014000103A mov [rsp+28h], rax ;[rsp+28h]存放n的地址,int& ref = n 000000014000103F mov rcx, [rsp+28h] ;参数1传递n的地址 0000000140001044 call sub_140001000 ;调用add函数 0000000140001049 xor eax, eax 000000014000104B add rsp, 38h 000000014000104F retn //x64_gcc对应汇编代码讲解 000000000040156A push rbp 000000000040156B mov rbp, rsp 000000000040156E sub rsp, 30h 0000000000401572 mov [rbp+10h], ecx 0000000000401575 mov [rbp+18h], rdx 0000000000401579 call __main ;调用初始化函数 000000000040157E mov dword ptr [rbp-0Ch], 12345678h;n=0x12345678 0000000000401585 lea rax, [rbp-0Ch] ;rax=&n 0000000000401589 mov [rbp-8], rax ;[rbp-8]存放n的地址,int& ref = n 000000000040158D mov rax, [rbp-8] 0000000000401591 mov rcx, rax ;参数1传递n的地址,int * 0000000000401594 call _Z3addRi ;调用函数add(int &) 0000000000401599 mov eax, 0 000000000040159E add rsp, 30h 00000000004015A2 pop rbp 00000000004015A3 retn //x64_clang对应汇编代码讲解 0000000140001020 sub rsp, 48h 0000000140001024 mov dword ptr [rsp+44h], 0 000000014000102C mov [rsp+38h], rdx 0000000140001031 mov [rsp+34h], ecx 0000000140001035 mov dword ptr [rsp+30h], 12345678h;n=0x12345678 000000014000103D lea rdx, [rsp+30h] ;rdx=&n 0000000140001042 mov [rsp+28h], rdx ;[rsp+28h]存放n的地址,int& ref = n 0000000140001047 mov rcx, [rsp+28h] ;参数1传递n的地址 000000014000104C call sub_140001000 ;调用add函数 0000000140001051 xor eax, eax 0000000140001053 add rsp, 48h 0000000140001057 retn
在图2-10中可以看出,引用类型的存储方式和指针是一样的,都是使用内存空间存放地址值。所以,在C++中,除了引用是通过编译器实现寻址,而指针需要手动寻址外,引用和指针没有太大区别。指针虽然灵活,但如果操作失误将产生严重的后果,而使用引用则不存在这种问题。因此,C++极力提倡使用引用类型,而非指针。
引用类型也可以作为函数的参数类型和返回类型使用。因为引用实际上就是指针,所以它同样会在参数传递时产生一份备份,如代码清单2-7所示。
代码清单2-7 引用类型作为函数参数
//Add函数实现 void add(int &ref){ ref++; } //x86_vs对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 mov eax, [ebp+8] ;取出参数ref的内容放入eax 00401006 mov ecx, [eax] 00401008 add ecx, 1 0040100B mov edx, [ebp+8] 0040100E mov [edx], ecx ;对eax做取内容操作,间接访问实参 00401010 pop ebp 00401011 retn //x86_gcc对应汇编代码讲解 00401510 push ebp 00401511 mov ebp, esp 00401513 mov eax, [ebp+8] ;取出参数ref的内容放入eax 00401516 mov eax, [eax] 00401518 lea edx, [eax+1] 0040151B mov eax, [ebp+8] 0040151E mov [eax], edx ;对eax做取内容操作,间接访问实参 00401520 nop 00401521 pop ebp 00401522 retn //x86_clang对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 push eax 00401004 mov eax, [ebp+8] 00401007 mov ecx, [ebp+8] ;取出参数ref的内容放入ecx 0040100A mov edx, [ecx] 0040100C add edx, 1 0040100F mov [ecx], edx ;对ecx做取内容操作,间接访问实参 00401011 mov [ebp-4], eax 00401014 add esp, 4 00401017 pop ebp 00401018 retn //x64_vs对应汇编代码讲解 0000000140001000 mov [rsp+8], rcx 0000000140001005 mov rax, [rsp+8] ;取出参数ref的内容放入rax 000000014000100A mov eax, [rax] 000000014000100C inc eax 000000014000100E mov rcx, [rsp+8] 0000000140001013 mov [rcx], eax ;对rax做取内容操作,间接访问实参 0000000140001015 retn //x64_gcc对应汇编代码讲解 0000000000401550 push rbp 0000000000401551 mov rbp, rsp 0000000000401554 mov [rbp+10h], rcx 0000000000401558 mov rax, [rbp+10h] ;取出参数ref的内容放入rax 000000000040155C mov eax, [rax] 000000000040155E lea edx, [rax+1] 0000000000401561 mov rax, [rbp+10h] 0000000000401565 mov [rax], edx ;对rax做取内容操作,间接访问实参 0000000000401567 nop 0000000000401568 pop rbp 0000000000401569 retn //x64_clang对应汇编代码讲解 0000000140001000 push rax 0000000140001001 mov [rsp], rcx ;保存参数1 ref到局部变量空间 0000000140001005 mov rcx, [rsp] ;取出参数ref的内容放入rcx 0000000140001009 mov eax, [rcx] 000000014000100B add eax, 1 000000014000100E mov [rcx], eax ;对rcx做取内容操作,间接访问实参 0000000140001010 pop rax 0000000140001011 retn
在代码清单2-7中,通过对参数加1的方式修改实参数据。从汇编代码中可以看出,引用类型的参数也占用内存空间,其中保存的数据是一个地址值。取出这个地址中的数据并加1,再将加1后的结果放回地址,如果没有源码对照,指针和引用都一样难以区分。在反汇编下,没有引用这种数据类型。