前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >地址、指针与引用

地址、指针与引用

作者头像
Masimaro
发布于 2018-08-31 08:00:42
发布于 2018-08-31 08:00:42
70500
代码可运行
举报
运行总次数:0
代码可运行

计算机本身是不认识程序中给的变量名,不管我们以何种方式给变量命名,最终都会转化为相应的地址,编译器会生成一些符号常量并且与对应的地址相关联,以达到访问变量的目的。  

变量是在内存中用来存储数据以供程序使用,变量主要有两个部分构成:变量名、变量类型,其中变量名对应了一块具体的内存地址,而变量类型则表明该如何翻译内存中存储的二级制数。我们知道不同的类型翻译为二进制的值不同,比如整型是直接通过数学转化、浮点数是采用IEEE的方法、字符则根据ASCII码转化,同样变量类型决定了变量所占的内存大小,以及如何在二进制和变量所表达的真正意义之间转化。而指针变量也是一个变量,在内存中也占空间,不过比较特殊的是它存储的是其他变量的地址。在32位的机器中,每个进程能访问4GB的内存地址空间,所以程序中的地址采用32位二进制数表示,也就是一个整型变量的长度,地址值一般没有负数所以准确的说指针变量的类型应该是unsigned int 即每个指针变量占4个字节。还记得在定义结构体中可以使用该结构体的指针作为成员,但是不能使用该结构的实例作为成员吗?这是因为编译器需要根据各个成员变量的大小分配相关的内存,用该结构体的实例作为成员时,该结构体根本没有定义完整,编译器是不会知道该如何分配内存的,而任何类型的指针都只占4个字节,编译器自然知道如何分配内存。我们在书写指针变量时给定的类型是它所指向的变量的类型,这个类型决定了如何翻译所对应内存中的值,以及该访问多少个字节的内存。对指针的间接访问会先先取出值,访问到对应的内存,再根据指针所指向的变量的类型,翻译成对应的值。一般指针只能指向对应类型的变量,比如int类型的指针只能指向int型的变量,而有一种指针变量可以指向所有类型的变量,它就是void类型的指针变量,但是由于这种类型的变量没有指定它所对应的变量的类型,所以即使有了对应的地址,它也不知道该取多大内存的数据,以及如何解释这些数据,所以这种类型的指针不支持间接访问,下面是一个间接访问的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
    int nValue = 10;
    float fValue = 10.0f;
    char cValue = 'C';
    int *pnValue = &nValue;
    float *pfValue = &fValue;
    char *pcValue = &cValue;
    printf("pnValue = %x, *pnValue = %d\n", pnValue, *pnValue);
    printf("pfValue = %x, *pfValue = %f\n", pfValue, *pfValue);
    printf("pcValue = %x, *pcValue = %c\n", pcValue, *pcValue);
    return 0;
}

下面是它对应的反汇编代码(部分):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
10:       int nValue = 10;
00401268   mov         dword ptr [ebp-4],0Ah
11:       float fValue = 10.0f;
0040126F   mov         dword ptr [ebp-8],41200000h
12:       char cValue = 'C';
00401276   mov         byte ptr [ebp-0Ch],43h
13:       int *pnValue = &nValue;
0040127A   lea         eax,[ebp-4]
0040127D   mov         dword ptr [ebp-10h],eax
14:       float *pfValue = &fValue;
00401280   lea         ecx,[ebp-8]
00401283   mov         dword ptr [ebp-14h],ecx
15:       char *pcValue = &cValue;
00401286   lea         edx,[ebp-0Ch]
00401289   mov         dword ptr [ebp-18h],edx
16:       printf("pnValue = %x, *pnValue = %d\n", pnValue, *pnValue);
0040128C   mov         eax,dword ptr [ebp-10h]
0040128F   mov         ecx,dword ptr [eax]
00401291   push        ecx
00401292   mov         edx,dword ptr [ebp-10h]
00401295   push        edx
00401296   push        offset string "pnValue = %x, *pnValue = %d\n" (00432064)
0040129B   call        printf (00401580)
004012A0   add         esp,0Ch

从上面的汇编代码可以看到指针变量会占内存空间,它们的地址分别是:[ebp - 10h] 、 [ebp - 14h]、 [ebp - 18h],在给指针变量赋值时首先将变量的地址赋值给临时寄存器,然后将寄存器的值赋值给指针变量,而通过间接访问时也经过了一个临时寄存器,先将指针变量的值赋值给临时寄存器(mov     eax,dword ptr [ebp-10h])然后通过这个临时寄存器访问变量的地址空间,得到变量值(    mov         ecx,dword ptr [eax]),由于间接访问进过了这几步,所以在效率上是比不上直接使用变量。下面是对char型变量的间接访问:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
004012BF   mov         edx,dword ptr [ebp-18h]
004012C2   movsx       eax,byte ptr [edx]
004012C5   push        eax

首先也是将指针变量的值取出来,放到寄存器中,然后根据寄存器寻址找到变量对应的地址,访问变量。其中”bye ptr“表示只操作该地址中的一个字节。

对于地址我们可以进行加法和减法操作,地址的加法主要用于向下寻址,一般用于数组等占用连续内存空间的数据结构,一般是地址加上一个数值,表示向后偏移一定的单位,指针同样也有这样的操作,但是与地址值不同的是指针每加一个单位,表示向后偏移一个元素,而地址值加1则就是在原来的基础上加上一。指针偏移是根据其所指向的变量类型来决定的,比如有下面的程序:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main(int argc, char* argv[])
{
    char szBuf[5] = {0x01, 0x23, 0x45, 0x67, 0x89};
    int *pInt = (int*)szBuf;
    short *pShort = (short*)szBuf;
    char *pChar = szBuf;

    pInt += 1;
    pShort += 1;
    pChar += 1;
    return 0;
}

它的汇编代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
9:        char szBuf[5] = {0x01, 0x23, 0x45, 0x67, 0x89};
00401028   mov         byte ptr [ebp-8],1
0040102C   mov         byte ptr [ebp-7],23h
00401030   mov         byte ptr [ebp-6],45h
00401034   mov         byte ptr [ebp-5],67h
00401038   mov         byte ptr [ebp-4],89h
10:       int *pInt = (int*)szBuf;
0040103C   lea         eax,[ebp-8]
0040103F   mov         dword ptr [ebp-0Ch],eax
11:       short *pShort = (short*)szBuf;
00401042   lea         ecx,[ebp-8]
00401045   mov         dword ptr [ebp-10h],ecx
12:       char *pChar = szBuf;
00401048   lea         edx,[ebp-8]
0040104B   mov         dword ptr [ebp-14h],edx
13:
14:       pInt += 1;
0040104E   mov         eax,dword ptr [ebp-0Ch]
00401051   add         eax,4
00401054   mov         dword ptr [ebp-0Ch],eax
15:       pShort += 1;
00401057   mov         ecx,dword ptr [ebp-10h]
0040105A   add         ecx,2
0040105D   mov         dword ptr [ebp-10h],ecx
16:       pChar += 1;
00401060   mov         edx,dword ptr [ebp-14h]
00401063   add         edx,1
00401066   mov         dword ptr [ebp-14h],edx

根据其汇编代码可以看出,对于int型的指针,每加1个会向后偏移4个字节,short会偏移2个字节,char型的会偏移1个,所以根据以上的内容,可以得出一个公式:TYPE* P p + n = p + sizeof(TYPE) *n

根据上面的加法公式我们可以推导出两个指针的减法公式,TYPE *p1, TYPE* p2: p2 - p1 = ((int)p2 - (int)p1) / sizeof(TYPE),两个指针相减得到的结果是两个指针之间拥有元素的个数。只有同类型的指针之间才可以相减。而指针的乘除法则没有意义,地址之间的乘除法也没有意义。

  引用是在C++中提出的,是变量的一个别名,提出引用主要是希望减少指针的使用,引用于指针在一个函数中想上述例子中那样使用并没有太大的意义,大量使用它们是在函数中,作为参数传递,不仅可以节省效率,同时也可以传递一段缓冲,作为输出参数来使用。这大大提升了程序的效率以及灵活性。但是在一些新手程序员看来指针无疑是噩梦般的存在,所以C++引入了引用,希望代替指针。在一般的C++书中都说引用是变量的一个别名是不占内存的,但是我通过查看反汇编代码发现引用并不是向书上说的那样,下面是一段程序及它的反汇编代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int nValue = 10;
int &rValue = nValue;
printf("%d\n", rValue);
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
10:       int nValue = 10;
00401268   mov         dword ptr [ebp-4],0Ah
11:       int &rValue = nValue;
0040126F   lea         eax,[ebp-4]
00401272   mov         dword ptr [ebp-8],eax
12:       printf("%d\n", rValue);
00401275   mov         ecx,dword ptr [ebp-8]
00401278   mov         edx,dword ptr [ecx]
0040127A   push        edx
0040127B   push        offset string "%d\n" (0042e01c)
00401280   call        printf (00401520)

从汇编代码中可以看到,在定义引用并为它赋值的过程中,编译器其实是将变量的地址赋值给了一个新的变量,这个变量的地址是[ebp - 8h],在调用printf函数的时候,编译器将地址取出并将它压到函数栈中。下面是将引用改为指针的情况:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
10:       int nValue = 10;
00401268   mov         dword ptr [ebp-4],0Ah
11:       int *pValue = &nValue;
0040126F   lea         eax,[ebp-4]
00401272   mov         dword ptr [ebp-8],eax
12:       printf("%d\n", *pValue);
00401275   mov         ecx,dword ptr [ebp-8]
00401278   mov         edx,dword ptr [ecx]
0040127A   push        edx
0040127B   push        offset string "%d\n" (0042e01c)
00401280   call        printf (00401520)

两种情况的汇编代码完全一样,也就是说引用其实就是指针,编译器将其包装了一下,使它的行为变得和使用变量相同,而且在语法层面上做了一个限制,引用在定义的时候必须初始化,且初始化完成后就不能指向其他变量,这个行为与常指针相同。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-10-24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
数组的剖析
C语言中数组是十分重要的一种结构,数组采用的是连续存储的方式,下面通过反汇编的方式来解析编译器对数组的操作。
Masimaro
2018/08/31
6360
滴水逆向初级-C语言(二)
1、声明变量 变量类型变量名; 变量类型用来说明宽度是多大 int 4个字节 short 2个字节 char 1个字节
zhang_derek
2021/04/13
1.3K0
C++继承分析
面向对象的三大特性之一就是继承,继承运行我么重用基类中已经存在的内容,这样就简化了代码的编写工作。继承中有三种继承方式即:public protected private,这三种方式规定了不同的访问权限,这些权限的检查由编译器在语法检查阶段进行,不参与生成最终的机器码,所以在这里不对这三中权限进行讨论,一下的内容都是采用的共有继承。
Masimaro
2019/02/25
5330
C++多态
面向对象的程序设计的三大要素之一就是多态,多态是指基类的指针指向不同的派生类,其行为不同。多态的实现主要是通过虚函数和虚表来完成,虚表保存在对象的头四个字节,要调用虚函数必须存在对象,也就是说虚函数必须作为类的成员函数来使用。 编译器为每个拥有虚函数的对象准备了一个虚函数表,表中存储了虚函数的地址,类对象在头四个字节中存储了虚函数表的指针。 下面是一个具体的例子
Masimaro
2019/02/25
3670
C++类的构造函数与析构函数
C++中每个类都有其构造与析构函数,它们负责对象的创建和对象的清理和回收,即使我们不写这两个,编译器也会默认为我们提供这些构造函数。下面仍然是通过反汇编的方式来说明C++中构造和析构函数是如何工作的。
Masimaro
2018/08/31
1.6K0
汇编角度看函数堆栈调用
带着以下一个问题来探索: (1)形参的内存空间的开辟和清理是由调用方还是由被调用方执行的? (2)主函数调用函数结束后,主函数从哪里开始执行?从头开始还是从调用之后开始? (3)返回值是如何带出来的?
lexingsen
2022/02/24
6750
汇编角度看函数堆栈调用
引用的条件及从汇编角度理解引用
(1)定义引用时必须进行初始化。 (2)初始化的值要能取地址,不能用一个立即数进行初始化。
lexingsen
2022/02/24
5390
引用的条件及从汇编角度理解引用
C函数原理
C语言作为面向过程的语言,函数是其中最重要的部分,同时函数也是C种的一个难点,这篇文章希望通过汇编的方式说明函数的实现原理。
Masimaro
2018/08/31
6090
C++对象模型_operator delete异常分析
开发环境 VC6.0 编辑器 Cmd Markdown C++中delete表达式执行的操作是:1,调用析构函数;2,释放对象内存(operator delete(…))。 如果父类的析构函数没有声明为virtual函数,且子类中至少存在一个virtual函数,此时将子类的对象地址赋值给父类指针。当对父类的指针执行delete操作时,会调用父类析构函数,然后在释放内存时(即delete表达式执行的操作的2,释放对象内存)出现崩溃。然而如果子类中不存在一个virtual函数时,执行上面同样的操作就不
chinchao.xyz
2022/04/25
5740
C++对象模型_operator delete异常分析
C++对象模型_Class Obj作为函数参数
开发环境 VC6.0 编辑器 Cmd Markdown 关于C/C++中基本类型(如:int,int*等)作为函数参数时,是通过将该变量的值压栈来进行参数传递;本文通过C++反汇编代码分析了当对象作为函数参数时(该形参非引用或指针),参数如何传递以及此时栈帧的结构。 对象作为函数参数时,参数传递过程(如:函数的声明为:void show(class Object obj);该函数的调用的为show(arg);其中实参arg的类型为class Object):1,在栈顶上为obj对象分配内存空间,然
chinchao.xyz
2022/04/25
1.2K0
C++对象模型_Class Obj作为函数参数
C语言循环的实现
在汇编代码中首先执行了一次循环体中的操作,然后判断,当条件满足时会跳转回循环体,然后再次执行,当条件不满足时会接着执行后面的语句。 这个过程可以用goto来模拟:
Masimaro
2018/08/31
2.5K0
PC逆向之代码还原技术,第五讲汇编中乘法的代码还原
在汇编中,乘法指令使用 IMUL 或者 MUL指令. 一般有两种形式 IMUL reg,imm 这种指令格式是 reg * imm的结果 重新放到reg中. mul同上 第二种指令格式: IMUL reg,reg1,imm 这种形式是 reg1寄存器 * imm的结果.放到reg中.
IBinary
2019/05/25
9430
C语言与汇编的嵌入式编程:求100以内素数
 由于C语言中使用的是for进行循环,使用VC调试汇编时,发现for汇编的jmp需要具体地址才可以进行,对于程序来讲不方便
墨文
2020/02/28
2K0
C语言与汇编的嵌入式编程:求100以内素数
你一定要搞明白的C函数调用方式与栈原理
这绝对不是标题党。而是C/C++开发中你必须要掌握的基础知识,也是高级技术岗位面试中高频题。我真的真的真的希望无论是学生还是广大C/C++开发者,都该掌握此文中介绍的知识。
范蠡
2018/07/25
3.4K0
你一定要搞明白的C函数调用方式与栈原理
汇编代码还原第一讲,基本类型以及浮点编码.
C++中整数的基本数据类型有三种, int long short. 在 VC6.0中,int long所占内存都是4字节. short两个字节. 以16进制为例 int long 分别就是4个字节. short两个字节. 一个字节是8位.
IBinary
2019/05/25
1.2K0
初探windows异常处理
首发于奇安信攻防社区:https://forum.butian.net/share/1475
红队蓝军
2022/05/17
5210
初探windows异常处理
堆栈基础(一)
本文稿费80软妹币 砸个广告:各位在网络安全方面有新创作的小伙伴,快将你们的心得砸过来吧~ 文章以word形式发至邮箱: minwei.wang@dbappsecurity.com.cn 有偿投稿,
安恒网络空间安全讲武堂
2018/06/26
7560
【C语言】汇编角度剖析函数调用的整个过程
压栈操作,他会改变esp所指向的位置,从而适应栈帧空间的扩大,操作方式就是将操作数直接压栈到栈帧空间
举杯邀明月
2023/04/12
1.6K0
【C语言】汇编角度剖析函数调用的整个过程
C语言与汇编的嵌入式编程:main中模拟函数的调用(两数交换)
注明:swap函数大致处理过程为:把下个地址压入堆栈,然后参数入栈,然后把所有寄存器压入堆栈,分配空间,空间清C然后变量赋值开始程序然后做堆栈平衡清理堆栈
墨文
2020/02/28
9910
函数栈帧的创建与销毁
最近在学习C语言的过程中遇到了一些问题,在询问老师和查询相关资料的基础上了解到了函数栈帧的相关概念,对下列问题也有了答案。
摘星
2023/04/28
5350
函数栈帧的创建与销毁
相关推荐
数组的剖析
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验