文章目录
C中的指针与函数有着密切的关联,利用指针可以将数据传递给函数,并且允许函数对数据进行修改。而对于大多数块结构的语言,函数的调用和返回其实就是程序栈中栈帧的压栈和弹栈。调用该函数将创建的栈帧压入程序栈,函数调用完毕,程序栈中弹出栈帧。
1. 程序的栈和堆区
程序的栈和堆是C运行时元素。
1.1 程序栈
首先,程序栈作为支持函数执行的一块内存区域,通常和堆共享。程序栈通常占据这块共享区域的下部,而堆占据的是上部。
程序栈功能就是用来存放由各个函数和局部变量开辟的栈帧。栈帧中存放函数参数和局部变量。
void fun1(){
int *p = ...;
int pl;
}
void fun2(){
int *pp = ...;
int fun1();
}
int main(){
int k;
fun2();
}
以上程序,执行main函数,压入程序栈,该栈帧中存有局部变量k;接着调用fun2函数,继续入程序栈,该栈帧,存有指针变量pp,指向一块内存区域,随着语句加载,调用fun1,fun1函数栈帧入栈,其中一个指针变量,另一个普通变量。语句顺序加载,栈向上“生长”。当函数终止时,栈帧从程序栈弹出。而栈帧使用的内存不会被清理。但最终可能会被推到程序栈的另一个栈覆盖。
上述代码程序栈图:
1.2 栈帧
栈帧的组成:
- 返回地址
函数调用完毕后要返回的程序内部地址 - 局部数据存储域
为局部变量分配的内存 - 参数存储
为函数参数分配的内存 - 栈指针和基指针
运行时系统用来管理栈的指针
栈指针一般指向栈顶部。基指针(栈指针)通常存在并指向栈帧内部的地址,如返回地址,用来协助访问栈帧内部的元素。不过这两个指针都不是C指针,它们是运行时系统管理程序栈的地址。
利用如下函数说明栈帧的创建。
float avg(int *array,int size){
int sum = 0;
printf("array: %p\n",&array);
printf("size: %p\n",&size);
printf("sum = %p\n",&sum);
for(int i = 0;i < size;i++){
sum += array[i];
}
return (sum * 1.0f) / size;
}
程序输出:
可以看出参数和局部变量地址之间存在空挡,这一块内存中保存的是系统管理栈所需要的其他栈帧元素。
系统在创建栈帧时,将参数以跟声明时相反的顺序推到帧上,最后推入局部变量,如下图所示。在这个例子中,size先于array被推入程序栈。通常,接下来会推入函数调用的返回地址,最后是局部变量。推入它们的顺序与其在代码中列出的顺序相反。
for语句中的变量i没有包含到栈中。C把语句块作为“微型”函数,在合适的时机将其推入栈中。在上述代码中,块语句在执行时被推入程序栈中avg的栈帧上部,执行完毕后弹出程序栈。
这样将栈帧推到程序栈上时,可能引发的一个问题就是耗尽内存,这种情况称之为内存溢出,通常会导致程序的非正常终止。每个线程通常都有自己的程序栈。一个或多个线程访问内存中的同一个对象时可能会发生内存访问冲突。
2. 通过指针传递和返回数据
通过传递指针可以让多个函数访问指针所引用的对象,而不需把该对象声明为全局可访问。这么做也就意味着只有需要访问这个对象的函数才有权限访问,并且传递指针也不用复制对象。
当涉及到大型数据结构时,传递参数的指针效率更高。比如要传入一个结构体给函数,如果直接传参结构体,那么就需要复制整个结构体,势必会导致内存和时间上的开销。而传递结构体指针则不同,不需要复制对象,但可以通过指针访问对象。
2.1 指针传递数据
使用指针传递数据的一个主要原因就是可以修改数据。例如C语言中可以使用指针实现交换函数。
如下:
void swapT(int *p1,int *p2){
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
指针p1和p2在交换的操作中被解引,通过引入第三个变量完成两个变量的交换。而若是传入两个普通变量,则无法完成交换,因为在函数中保存的只是实参的副本,对其进行修改,并不会改变实参的值。实参的副本在函数调用之后也被弹出栈,即不能完成两数的交换。
2.2 传递指向常量的指针
传递指向常量的指针是C中常用的技术之一。这样做的目的就是:传递过去的数据只可被读取,而不能被更改。
下面用一个例子说明指向常量的指针和指向整数的指针:
void func(const int* num1,int *num2){
*num2 = *num1;
}
int main(){
const int a=1999;
int result;
func(&a,&result);
return 0;
}
执行上述代码,func函数会把1999赋给result
变量。
但如果试图去修改指向常量的指针:
void func2(const int* num1,int* num2){
*num1 = 1999;
*num2 = 715;
}
若执行func2则会报错,原因是试图去修改指针所引用的常量。即不能去修改一个非左值。
2.3 返回指针
编程时常常会调用其他函数,而函数返回对象时经常用到下面两种技术:
- 使用
malloc
在函数内部分配内存并返回地址。调用者负责释放返回的内存。 - 传递一个对象给函数并让函数对其进行修改。这样分配和释放内存都是调用者的责任。
下面这个例子,定义一个函数,传入一个整数数组的长度和一个值来初始化每个元素。
函数为整数数组分配内存,用传入的值进行初始化,最后返回数组地址。
int *arr(int size,int value){
int* a = (int*)malloc(size * sizeof(int));
for(int i = 0;i < size;i++){
a[i] = value;
}
return a;
}
int main(){
int *v = arr(5,0);
for(int i = 0;i < 5;i++){
printf("%d\t",v[i]); //输出五个0
}
}
调用者调用返回指针的函数之后,有责任释放内存,否则会造成内存泄漏。
2.4 局部数据指针
在编程中我们可能会遇到一类错误:返回指向局部数据的指针错误。
int* arr(int size,int value){
int a[size];
for(int i = 0;i < size;i++){
a[i] = value;
}
return a;
}
当该函数返回时,返回的数组地址也就无效了,因为函数结束,函数栈帧从程序栈中弹出。尽管每个数组元素可能包含原来所赋值,但若调用另一个函数,值就可能被覆盖了。
另一种方法就是将数组变量声明为static
。这样就会把变量的作用域限制在函数内部。但是分配在栈帧外部,可以避免被其他函数覆写变量值。
int* arr(int size,int value){
static int a[5]; //静态数组必须声明为固定长度
}
2.5 传递空指针
编程中,传递指针会经常遇到,但在使用其之前进行判空是一个很好的习惯,否则,若指针为空,就会导致程序的非正常终止。
int* arr(int* a,int size,int value){
if(a != NULL){ //使用前先进行判空
for(int i = 0;i < size;i++){
a[i] = value;
}
}
}
2.6 传递指针的指针
将指针传递给函数时,传递的是值。如果我们要修改原指针而不是原指针的副本,就需要传递指针的指针。下面例子传递一个整数数组的指针,为该数组分配内存并将其初始化。
void allocateArray(int **arr,int size,int value){
*arr = (int*)malloc(size * sizeof(int));
if(arr !=NULL){
for(int i = 0;i < size;i++){
*(*arr+i) = value;
}
}
}
/*测试代码*/
int* v=NULL;
int* v=allocateArray(&v,5,10);
测试代码中第一个参数以整数指针的指针的形式传递。如果要调用这个函数,我们只能传递这种类型的值。malloc
返回的地址赋予arr。解引整数指针的指针得到整数指针。因为是v的地址,所以我们对其做了修改。
而如果传递一个指针则就不会起作用:
void allocateArray(int* arr,int size,int value){
arr = (int*)malloc(size * sizeof(int));
if(arr !=NULL){
for(int i = 0;i < size;i++){
arr[i] = value;
}
}
}
/*测试代码*/
int *v = NULL;
allocateArray(v,5,10);
printf("%p\n",v);
将v传递给函数时,它的值被复制到参数arr
中,对arr进行修改并不会影响v。当函数返回时,没有将存储在arr中的值复制到v中。其内存分配图如下:
从中间的图可看出arr
变量指向了堆中的另一个新的位置。从最右侧的图可以看出存在内存泄漏:地址为600的空间无法再被访问。
释放内存的函数很多时候需要我们自己定义。free函数本身不会检查传入的指针是否为NULL
,也并不会在返回前把指针置为NULL
。下面是内存的释放函数:
void myFree(void** p){ //参数void指针的指针
if(p != NULL && *p != NULL){
free(*p);
*p = NULL;
}
}
使用指针的指针允许我们修改传入的指针,而使用void类型可以传入所有类型的指针。
对上述myFree
函数进行测试:
int main(){
int* pi;
pi = (int*)malloc(sizeof(int));
*pi = 1999;
printf("Before: %p\n", pi);
myFree((void**)&pi);
printf("After: %p\n", pi);
myFree((void**)& pi);
return (EXIT_SUCCESS);
}
输出结果为:
即使传入一个NULL
指针,程序仍不会非正常终止。
3. 函数指针
首先什么是函数指针?
即持有函数地址的指针,函数指针对于C语言来说是极为重要的一项特性。为我们在编译时未确定的顺序执行函数提供了另一种选择,即不通过使用条件语句也可以完成。
3.1 声明函数指针
声明一个函数指针,接受一个空的参数,并返回空值
void (*fp)();
这个函数指针的声明很像函数原型声明。若去掉第一对括号,看起来就像函数的原型,接受一个void
值,返回一个void
指针。不过,括号让这个声明变成了一个名为fp
的函数指针。*
表示是一个指针。fp
表示函数指针变量的名字。
注意 使用函数指针要小心,因为C不会检查参数传递是否正确。
int (*fptr1)(double); //传入double,返回int的
void (*fptr2)(char*); //传入char*,无返回值
double* (*fptr3)(int,int); //传入int,int返回double型指针
在学习中一定要注意,返回指针的函数和函数指针是两个不同的概念。
int* f4(); //f4是一个函数,返回的是整数指针
int (*fptr5)(); //fptr5是返回整数的函数指针
int* (*fptr6)(); //fptr6是返回整数指针的函数指针
很明显,f4是一个返回 指针的函数,而fptr5
的括号则明确的把表示“指针”的*
和函数名绑定在一起,所以fptr5
是一个函数指针。
3.2 使用函数指针
int (*fptr1)(int);
int func(int num){
return num * num;
}
上述是使用函数指针的一个例子,函数接受一个整型参数并返回一个整数,实现求一个整数的平方和的功能。
现在我们使用函数指针来调用函数,需要做的就是把func
函数的地址赋给函数指针变量fptr1
,即如下:
int num = 10;
fptr1 = func; //犹如数组名,直接使用函数名本身
printf("%d\n",fptr1(num)); //输出 100
同样我们也可以对函数取地址进行赋值。但一般不会这么做,因为编译过程中编译器会自动忽略取地址符。
为方便起见,我们经常做这样一个操作,即为函数指针声明一个类型定义。尽管它看起来不是那么友好。通常类型定义的名字是声明的最后一个一个元素。
typedef int (*fptr0)(int);
fptr0 pf;
p2 = fptr1;
printf("%d\n",p2(num)); //同样输出10
3.3 传递函数指针
即把函数指针作为参数在函数间传递。下面使用add,sub,compute
函数来说明如何传递函数指针:
int add(int num1,int num2){
return num1 + num2;
}
int sub(int num1,int num2){
return num1 - num2;
}
typedef int (*fptrOper)(int,int);
int compute(fptrOper f,int num1,int num2){
return f(num1,num2);
}
使用:
printf("%d\n",compute(add,5,10)); //输出15
printf("%d\n",compute(sub,10,5)); //输出5
解释:将add
和sub
函数地址传递给compute
函数,该函数中使用传递过来的函数地址调用相应的函数,实现对应的功能。类似于java
中的多态,C语言中使用函数指针无疑会使得代码更为轻便灵活。
3.4 返回函数指针
返回函数指针需要把函数类型声明为函数指针。如下:
fptrOper select(char opcode){
switch(opcode) {
case '+': return add;
case '-': return sub;
}
}
上述的select
函数传入一个字符,返回一个指向对应函数的指针。如果传入+
,则返回指向add
函数的指针,传入-
,返回指向sub
函数的指针.
int calculate(char opcode,int num1,int num2){
fptrOper op = select(opcode);
return op(num1,num2);
}
calculate
函数接受一个字符和两个整数,字符表示要进行的操作,它将字符opcode
传递给select
函数,并返回要执行的函数指针。通过函数指针去调用对应函数,并返回结果。
calculate
函数使用如下:
printf("%d\n",calculate('+' ,10, 5)); //输出15
printf("%d\n",calculate('-' ,10, 5)); //输出5
3.5 使用函数指针数组
函数指针数组可以基于某些条件选择要执行的函数。其声明也很简单,只需把函数指针声明为数组类型即可。如下:
typedef int (*opt)(int,int);
opt optrs[128] = {NULL};
//或者不适用typedef:
int (*optrs[128])(int,int) = {NULL};
数组的所有元素都被初始化为NULL
。如果数组的初始化值是一个语句块,系统会将块中的值赋给连续的数组元素。
上述函数数组的目的就是可以用一个字符索引选择对应的函数执行。比如,若果存在*
字符就表示乘法函数,我们可以用字符作为索引是因为字符字面量值其实是整数。
函数指针数组初始化为NULL
之后,我们可以把相应的操作符号赋给数组元素.
void initOper(){
optrs['+'] = add;
optrs['-'] = sub;
}
接下来,通过操作字符作为索引来使用optrs
,而不是调用select
函数获取函数指针。
int calculateArray(char opcode,int num1,int num2){
fptrOper op;
op = optrs[opcode];
return op(num1,num2);
}
代码测试:
initOper();
printf("%d\n",calculateArray('+',10,5)); //输出10
printf("%d\n",calculateArray('-',10,5)); //输出5
3.6 比较函数指针
可以使用相等和不等操作符来比较函数指针。如下代码,add
函数被赋给fptr1
函数指针,然后和add
函数地址作比较.:
fptrOper fptr1 = add;
if(fptr1 == add){
printf("fptr1 points to add functions\n");
}
else{
printf("fptr1 does not point to add functions\n");
}
通过执行,可以看出函数指针确实指向了add
。
比较函数指针用处的一个例子即:用函数指针数组表示一系列任务步骤的情况。
3.7 转换函数指针
即可以将指向某个函数的指针转换为指向其他类型的指针,不过要谨慎使用,因为运行时系统不会验证函数指针所用的参数是否正确。也可把一种函数指针转换为另一种再转换回来,得到的结果和原指针相同,不过函数指针的长度可能不同。
typedef int (*fptrToSingleInt)(int);
typedef int (*fptrToTwoInts)(int, int);
int add(int, int);
fptrToTwoInts fptr1 = add;
fptrToSingleInt fptr2 = (fptrToSingleInt)fptr1;
fptr1 = (fptrToTwoInts)fptr2;
printf("%d\n", fptr1(10, 5)); //输出15
void*
指针不一定可以用在函数上,也就是不能做如下操作:
void *pv = add;
不过在交换函数指针时,通常会见到如下声明所示的“基本”函数指针类型。把fptrBase
声明为指向不接受参数也不返回结果的函数的指针。
typedef void (*fptrBase)();
基本指针的用法如下:
fptrBase basePointer;
fptr1 = add;
basePointer = (fptrToSingleInt)fptr1;
fptr1 = (fptrToTwoInts)basePointer;
printf("%d\n",fptr1(10,5));
基本指针一般做占位符存在,用来交换函数的值。