函数调用方式与栈原理

内存模型

内存分为物理内存虚拟内存,物理内存对应计算机中的内存条,虚拟内存是操作系统内存管理系统假象出来的。由于这些不是我们本文的重点,下面不做区分。有不清楚这些概念的同学,可以给我留言或者在线询问。

计算机运行程序本质上就是对内存中的数据的操作,通俗地来说,就是将内存条某些部分的数据搬进搬出或者搬来搬去,其中“搬进搬出”是指将内存中的二进制数据搬入cpu寄存器及运算器中进行相应的加减运算或者将寄存器中的数据搬回内存单元中,而“搬来搬去”是指将内存中的数据由这个位置搬到另外一个位置(当然,一般不是直接搬,而是借助寄存器作为中间存储区)。

 

 

指针的本意就是内存地址,我们可以通俗地理解成内存编号,既然计算机通过编号来操作内存单元,这就造就了指针的高效率

指针变量和指针常量

  • 指针变量首先是一个变量,由于指针变量存储了某个变量的内存首地址
  • 指针常量可通俗地理解为存储固定的内存单元地址编号的”量“,它一旦存储了某个内存地址以后,不可再改存储其他的内存地址了。
  • 指针常量一般用在函数的参数中,表示该函数不可改变实参的内容。

  • *(a + 1) = 2, *(ptr - 1) = 5。
  • 这是一个3行4列的数组,它在内存中的分布如下:
  • 所以arr和arr[0]和&arr[0][0]表示的都是同一个地址,但是这三个首地址在进行算术运算时是有区别的。如果&arr[0][0] + 1,这里的相当于跳一个元素的内存字节数,也就是4个;但是arr[0] + 1,移动的内存字节数是一列元素所占的字节数,也就是4 4 = 16个;最后,也是最让人迷惑的的就是arr + 1,这个时候移动的内存数目是整个数组占的内存字节数,也就是48个字节数,所以a + 1所表示的内存地址已经不属于这个数组了,这个地址位于数组最后一个元素所占内存空间的*下一个字节空间
  • void类型更多是用来”修饰“”限制“一个函数的:例如一个函数如果不返回任何类型的值,可以用void作返回类型;如果一个函数无参数列表,可以用void作为参数列表
  • 跟void类型”修饰“作用不同,void型指针作为指向抽象数据的指针,它本质上表示一段内存块。如果两个指针类型不同,在进行指针类型赋值时必须进行强制类型转换
  •  
  • 但是可以将任何指针类型赋值给void类型而无须进行强制类型转换:
  •  
     void *memcpy(void *dest,const void *src,size_t len);
    
  • 在这里,任何数据类型的指针都可以传给这个函数,所以这个函数成为了一个通用的内存复制函数。

    好了,说了这么多,回答最初的那个问题上:

函数指针

  • 函数指针是指向函数的指针变量
  • 每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。
  • 有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样
  • 用途:调用函数做函数的参数

通过上面的例子,我们来总结下函数指针的定义和使用方法:

首先,通过typedef关键字定义一个函数指针类型,然后定义一个该函数指针类型变量,接着将函数的入口地址赋值给该函数指针类型变量,这样就可以通过这个函数指针变量调用函数了。

函数指针的另外一个用处,而且是用的最多的,就是作为一个函数的参数。也就是说某个函数的某个参数类型是一个函数,这在windows编程中作为回调函数(callback)尤其常见。我们来看一个例子:

 

其实定义指针变量的时候,星号(*)无论是与数据类型结合还是与变量名结合在一起都是一样的!但是,为了便于理解,还是推荐大家写成第一种形式,第二种形式容易误导人,不是吗?而且第一种形式还有一个好处,我们可以这样看:

int *a;  //将*a看成一个整体,它是一个int型数据,那么a自然就是指向*a的指针了。

说完定义指针的方法,下面我们来看下如何初始化一个指针变量,看下面的代码:

 

错误在于我们不能这样写:int *p = 1; 由于p是一个匿名指针,也就是说p没有正确的初始化,它可能指向一个不确定的内存地址,而这个内存地址可能是系统程序内存所在,我们将数值1装入那个不确定的内存单元中是很危险的,因为可能会破坏系统那个内存原来的数据,引发异常。换另一个方面来看,将整型数值1直接赋值给指针型变量p是非法的。

这样的指针我们称为匿名指针或者野指针。和其他变量类型一样,为了防止发生意料之外的错误,我们应该给新定义的指针变量一个初始值。但是有时候,我们又没有合适的初始值给这个指针,怎么办?我们可以使用NULL关键字或者C++中的nullptr。代码如下:

 

C++中的引用

所谓引用,使用另外一个变量名来代表某一块内存,也就是说a和b完全是一样,所以任何地方,可以使用a的,换成b也可以,而不是使用&b,这就相当于同一个人有不同的名字,但是不管哪个名字,指的都是同一个人。

 

传值还是传引用(by value or by reference)

如果变量类型是基元数据类型(基础数据类型),比如int、float、bool、char等小数据类型被称为基元数据类型(primitive data type),那么赋值时传的是值。也就是说,这个时候b的值是a的拷贝,那么更改b不会影响到a,同理更改a也不会影响到b。

但是,如果变量类型是复杂数据类型(complex data type),不如数组、类对象,那么赋值时传的就是引用,这个时候,a和b指向的都是同一块内存区域,那么无论更改a或者b都会相互影响

  •  为什么各大语言都采取这种机制。对于那些基元数据类型,由于数据本身占用的内存空间就小,这样复制起来不仅速度快,即使这样的变量数目很多,总共也不会占多大空间。
  • 是对于复杂数据类型,比如一些类对象,它们包含的属性字段就很多,占用的空间就大,如果赋值时,也是复制数据,那么一个两个对象还好,一旦多一点比如10个、100个,会占很大的内存单元的,这就导致效率的下降。
  • 但是对于引用类型数据,我们需要自己实现引用型数据的真正复制。

C/C++中的new关键字与Java、C#中的关键字对比

  • 对于C/C++中new出来的对象必须通过指针对象来引用它非常不习惯。上图中,Object是一个(class),在Java或者C#等语言中,通过new关键字定义一个对象,直接得到Object的实例,也就是说后续引用这个对象,我们可以直接使用obj.property或者obj.method()
  • pObj->property或pObject->method()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值