天天看点

深入理解C指针之三:指针和函数

  理解函数和指针的结合使用,需要理解程序栈。大部分现代的块结构语言,比如C,都用到了程序栈来支持函数的运行。调用函数时,会创建函数的栈帧并将其推到程序栈上。函数返回时,其栈帧从程序栈上弹出。

  在使用函数时,有两种情况指针很有用。一种是将指针作为参数传递给函数,函数可以修改指针所引用的数据,可以高效的传递大块数据。另一种是声明函数指针。

  程序的栈和堆是C程序的重要运行时元素。程序栈是支持函数执行的内存区域,通常和堆共享一块内存区域。通常程序栈在区域的下部,堆在上部。程序栈存放栈帧(stack frame),栈帧有时候也称为活跃记录或活跃帧。栈帧存放函数参数和局部变量。堆则管理动态内存。

  调用函数时,函数的栈帧被推到栈上,栈向上“长出”一个栈帧。当函数终止时,其栈帧从程序上弹出。栈帧所使用的内存不会被清理,但有可能被另一个栈帧覆盖。

  栈帧由以下几种元素组成:

  * 返回地址。函数完成后要返回的程序内部地址。

  * 局部数据存储。为局部变量分配的内存。

  * 参数存储。为函数参数分配的内存。

  * 栈指针和基指针。运行时系统用来管理栈的指针。

  栈指针通常指向栈顶部。基指针(帧指针)通常存在并指向栈帧内部的地址,比如返回地址,用来协助访问栈帧内部的元素。这两个指针都是运行时系统用来管理程序栈的地址。系统在创建栈帧时,将参数以跟声明时相反的顺序推到帧上,最后推入局部变量。C把块语句当做“微型”函数,会在适当的时候将其推入栈和从栈上弹出。

  参数和局部变量的精确地址可能会变化,不过顺序一般不变。这一点可以解释参数和变量分配内存的相对顺序。将栈帧推到程序栈上时,系统可能会好近内存,这种情况称为栈溢出。要记住每个线程通常都有自己的程序栈,一个或多个线程访问内存中的同一个对象可能会导致冲突。

  传递参数(包括指针)的时候,传递的是它们的值的副本。当涉及大型数据结构时,传递参数的指针会更高效。传递对象的指针意味着不需要复制对象,但可以通过指针访问对象。

  用指针来传递数据的一个主要原因是函数可以修改数据。如果不希望函数修改数据,可以传递指向常量的指针。

  从函数返回指针有两种情况:一种是在函数内部为指针分配内存并返回指向内存的指针,另一种是函数的调用者负责指针内存的分配和释放。从函数返回指针可能存在几个潜在的问题:

  * 返回未初始化的指针。

  * 返回指向无效地址的指针。

  * 返回局部变量的指针。

  * 返回指针但是没有释放内存。

  设想一下在函数中声明一个指针并为该指针分配内存,但是在调用该函数的时候没有使用一个指针变量来接受函数的返回值,因此我们丢失了该分配内存的地址,导致内存泄露。

  局部数据指针是指当函数返回的时候,(非动态分配的)局部数据的地址也就无效了。动态分配的内存与此不同,存在于堆上,即使函数返回也不受影响。当变量为static,就会在栈帧外部为其分配内存,每次调用函数都会访问同一块内存。

  在将指针作为参数传递时,在使用之前判断指针是否为NULL是个好习惯。重复free一个指针会引发错误,在free之前也应判断指针是否为NULL。而且在释放之后将指针置为NULL。

  传递指针将允许我们修改指针指向的数据,如果我们想修改的是指针怎么办?当然是传递指向指针的指针了。

  函数指针是持有函数地址的指针。通过将函数指针作为参数传递,使我们避免将事件处理程序硬编码进函数里,更加灵活。

    void是返回类型,foo是指针变量名,后边的括号里是参数。

   使用函数的名字直接给函数指针赋值,它会把函数的地址赋给指针。也可以对函数名字使用取地址操作符,但是意思是一样的,在此处上下文中会忽略取地址操作符。可以为函数指针声明一个类型定义,通过类型定义来声明函数指针。

   传递函数指针只需把函数指针声明为参数即可。

   返回函数指针需要把函数的返回类型声明为函数指针。函数指针数组可以基于某些条件选择要执行的函数,声明这种数组只要把函数指针声明为数组的类型即可。

  我们可以用相等和不等操作符来比较函数指针。不同的函数指针的长度不一定相等。虽然可以将一种函数指针转换为另一种函数指针,但是应该谨慎使用这种方法,因为运行时系统不会验证函数指针所用的参数是否正确。不应该把函数指针转换成void*指针。基本指针用作占位符,用来交换函数指针的值。一定要确保给函数指针传递的参数是正确的,否则会造成不确定行为。

  总体来说,函数指针允许应用程序根据不同的条件执行不同的函数,对于控制应用程序内的执行序列很有用。