c++内存及函数传参

本人是一个 java 选手,用 java 的思想来理解 c++ 中的函数,还是有点别扭,所以本文先聊聊 c 和 java 很不一样的点,再来讲讲 c++ 中的函数

c 和 java 最不一样的点有两个,下面总结下二者在这方面的区别:

  • 内存回收时机
  • 内存回收机制

内存回收时机及机制

c 的内存一般分成三个逻辑区域:

  • 栈内存
  • 堆内存
  • 静态存储区

栈内存,就是各种临时变量,大部分的对象都属于栈内存对象,对于这种对象,c 比较火急火燎,属于有仇当场就报的那种,一旦过了此对象的作用域,其就会被回收

 {
    int num = 1;
 }

例如,上面的 num 变量,它的作用域就在那个花括号中,一旦方法执行超过了花括号,那么 num 变量就算走到头了,它就会被回收,所以说 c 是有仇当场就报了,不会像 java 那样

堆内存,简单来说就是通过 new 或者 malloc 申请的的内存,对于 c 开发者来说,主要管理这一块的内存。c 对这一块的内存,完全不管,完全由开发者自己管理,如果管理不善,比如忘记释放内存了,那么就会导致内存泄露,有时释放了内存,但指针没有置空,还会造成野指针的问题,严重会导致程序崩溃。

静态存储区,也分两种情况,如果是局部静态变量,它的生命周期是从程序第一次执行到它,到最后整个程序执行完毕,它才会被释放。

如果它是一个类静态变量,那么此静态变量,会随着这个类的第一个obj生成时开始,到整个程序执行完毕时,它才会被释放。

{
  int* ptr = new int;
}

说到这里,可能会有人有疑问了,上述代码中的 ptr 是一个局部变量,在栈中,它指向了一个堆内存,当 ptr 过了它的作用域时,它会被回收,此时还需要通过 delete 来释放 new 申请的内存吗?

答案是:必需要 delete。记住,ptr 自己是一个栈变量,它会被 c 回收,但它所指向的堆内存是不会被回收的,堆内存只能程序员自己手动回收。

这个逻辑就和 java 的很不一样了,如果有看过相关虚拟机的书,就知道 java 的内存回收原理是 引用计数法 或者 根结点引用法。这个方法的主要用处就是来判断堆中的变量要不要回收。java 中的内存回收,绝不只是回收栈中的变量,它连同栈中所指向的堆变量一并回收。这也是 java 比 c 方便的一点,不需要程序员自己处理内存,但这也并不是说 java 就没有内存问题了。

方法执行逻辑

int fun(int x) {
    ...
}
int a = 10;
int b = fun(a);

首先,c 中的方法执行,它是按传值来进行的,它是什么意思呢?比如说,上面的函数。它有一个形参 x,和一个 int 型的返回值。当调用 fun 函数时

  • 先生成一个临时变量 x,用 a 给 x 赋值,相当于 x = a,赋值之后, x 等于10
  • 最后返回时,也会生成一个临时变量 int temp,最后再用 temp 给 b赋值

所以,在函数执行的时候,会有这些问题产生:

  • 多次进行无用复制,比如 x,比如返回值 temp,例中只是 int 值,如果是一个大的数据或者是其它复杂数据,那么复制的代码也太大了,临时变量一会就被回收,但它却要在一段时间内占用大量内存,也会影响效率
  • 由于是按值传递,函数中真正操作的是 x,并不是 a,如果这是一个交换两个数的函数,执行完一看,尴尬了,两个数并没有交换,因为函数操作的不是它们,而是它们的复制品

理解了这个逻辑就知道了,一般而言,如果涉及到大的复杂的变量,一般是通过指针或者引用来作为函数的参数或者返回值,这样即使是复制,也只是复制了一个小小的指针或者引用。指针在 c 中,32位机器里4个字节,64位机器里,8个字节而已,简单得很。引用则更简单了,只是声明了一个别名而已。

但即使用了引用或者指针,就万事大吉了吗?不是的,它们还有别的限制

指针或引用形参或返回值

指针和引用作形参,一般而言,没啥问题,不过,如果函数对传入参数不作修改的话,为了安全起见,可以使用const 引用或 const指针

指针和引用在这里还有一个小区别,那就是,c++ 中,如果是数组,则必是用指针,因为数组名就是数组第一个元素的指针

{
  int* ptr;
  int array[10];
  ptr = array;
}

按上述代码,则 *(ptr + 1) == array[1] ,这两个是一样的。
但是,数组只能用指针来表示,不能用引用来表示,大家可以想想,从来没有用引用表示过数组

引用是什么呢,引用是一个变量的别名,它在一申明的时候就要被初始化。

c++中,到底何时使用按值传递,何时使用指针和引用呢?

  • 如果数据对象很小,如内置类型,则可以按值传递
  • 如果数据对象是数组,则只能使用指针
  • 如果数据是较大的结构,则使用指针或引用
  • 如果数据对象是类对象,则使用const引用,传递类对象参数的标准方式是按引用传递,这是 c++ 所推荐的,而不是指针,当然,指针也可以。
  • 如果数据对象还未被初始化,方法是用于初始化对象的,则是传递指针的指针

针对最后一点,大家可以自行想想,画画图,想想这是为什么

讲了这么多,貌似还是没有讲到指针或引用作为返回值,如果是指针和引用作用返回值,则需要看情况,如果处理不当,程序崩溃

int* fun() {
  int a;
  int* ptr = &a;
  return ptr;
}

如上例,a 是临时变量,返回的 ptr 是指向 a,函数执行完,a被回收,返回值岂不是指向了一个空的内存空间。这么做肯定是不行的。

那么,如果 修改下,int* ptr = new int ,这样做程序是不会崩溃了,因为 ptr指向的是堆内存,系统不会回收,程序员要记得自己回收即可。

同理,如果返回的是引用,则更加不能将引用指向一个临时变量了,因为引用即是别名,它的正主都已经被回收了,这个别名还能有什么用?

万恶的复制

其实 c++ 中的函数之所以这么麻烦,个人认为,和c中的复制逻辑有很大的关系。java中怎么不见这么费事呢,调用函数,参数直接传对象名就行了,因为java中的对象名,本质上也是指针。。。复制了,也就是复制个指针罢了

c++中,如果一个对象中既包含基础类型数据,也包含指针数据,那么这个类一定要重写复制函数。因为,如果类不写复制函数,系统会默认给类添加一个复制函数,但添加的默认复制函数,它会有一个问题,基础类型数据或者说非指针类型数据,都会给copy一份,但唯独指针数据,它只是重新构造一个指针,将指针也指向同一块内存区域。如果有两个对象,a 和 b,a是通过 b 复制得来的,那么 a 中的指针和 b 中的指针所指向的区域是同一块区域,如果 b中回收了指针所指的内存,则a 中的指针就变成了一个野指针,一旦使用野指针,程序崩溃没跑了。。。所以必须要重写复制函数

必须要重写复制函数了,但函数中参数还是按值传递呀,复制一次,还可能会复制堆内存变量,复制效率太低了,就不得不用指针或者引用了。

使用指针或引用,唯一需要注意一点,它们所指向的东西是不是临时的,会不会被立马回收

你可能感兴趣的:(c++内存及函数传参)