文章目录
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
在函数内部分配内存并返回地址。调用者负责释放返回的内存。 - 传递一个对象给函数并让函数对其进行修改。这样分配和释放内存都是调用者的责任。
下面这个例子,定义一个函数,传入一个整数数组的长度和一个值来初始化每个元素。
函数为整数数组分配内存,用传入的值进行初始化,最后返回数组地址。