局部变量也称为自动变量,他总是分配在栈帧上。
3.1 程序的堆和栈
3.1.1 程序栈
程序栈是支持函数执行的内存区域,通常和堆共享。也就是说,程序栈和堆共享同一块内存区域。程序栈通常占据这块区域的下部,而堆用的则是上部。
程序栈存放栈帧(stack frame),栈帧有时也称为活跃记录或活跃帧。栈帧存放函数参数和局部变量。堆管理动态内存。
调用函数时,函数的栈帧被推到栈上,栈向上“长出”一个栈帧。当函数终止时,其栈帧从程序栈上弹出。栈帧所使用的内存不会被清理,但最终可能被推到程序栈上的另一个栈帧覆盖。
动态分配的内存来自堆,对向下“生长”。随着内存的分配和释放,堆中会布满碎片。尽管堆是向下生长的,但这只是个大体方向,实际上内存可能在堆上的任意位置分配。
3.1.2 栈帧的组织
栈帧由以下几种元素组成:
- 返回地址:函数完成后要返回的程序内部地址
- 局部数据存储:为局部变量分配的内存
- 参数存储:为函数参数分配的内存
- 栈指针和基指针:运行时系统来管理栈的指针
栈指针通常指向栈顶部。基指针(帧指针)通常存在并指向栈帧内部的地址。这两个指针都不是C指针。它们是运行时系统管理程序栈的地址。
将栈帧推到程序栈上时,系统可能会耗尽内存,这种情况称为栈溢出。要牢记每个线程通常都会有自己的程序栈。
3.2 通过指针传递和返回数据
传递参数(包括指针)时,传递的是他们的值。也就是说,传递给函数的是参数的一个副本。当涉及大型数据结构时,传递参数的指针会高效。
3.2.1 用指针传递数据
void swap(int *pnum1, int *pnum2)
{
int tmp;
tmp = *pnum1;
*pnum1 = *pnum2;
*pnum2 = tmp;
}
3.2.2 用值传递数据
如果不通过指针传递参数,那么交换就不会发生。num1和num2中保存的只是参数的副本。修改形参不会影响实参。
void swap(int num1, int num2)
{
int tmp;
tmp = num1;
num1 = num2;
num2 = tmp;
}
3.2.3 传递指向常量的指针
如果只是传递指针,数据就能被修改,如果不希望数据被修改,就要传递指向常量的指针。
void pass(const int * num1,int num2) 不能修改通过指向常量的指针传进来的值
3.2.4 返回指针
函数返回指针时可能存在几个潜在的问题:
- 返回为初始化的指针
- 返回指向无效地址的指针
- 返回局部变量的指针
- 返回指针但是没有释放内存
3.2.5 局部数据指针
返回局部变量的指针,一旦函数执行完栈帧就被弹出了,尽管数据可能还在,但如果之后继续执行其他函数,该内存就会被覆盖。
如果把变量声明为static,这样把函数的作用域限制在函数内部,但是分配在栈帧外面,避免其他函数复写变量值。但每次调用该函数都会重复利用这个变量,这样相当于每次都把上一次调用的结果覆盖掉了。此外,静态数组必须声明为固定长度,这样会限制函数处理变长数组的能力。
3.2.7 传递指针的指针
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 *vector = NULL;
allocateArray(&vector, 5, 6);
实现自己的free函数:
void saferFree(void **pp){
if(pp != NULL && *pp != NULL){
free(*pp);
*pp = NULL;
}
}
使用指针的指针允许修改传入的指针,而使用void类型则可以传入所有类型的指针。如果调用这个函数时没有显示的把指针类型转换为void
会产生警告,执行显示转换就不会有警告。下面这个safeFree宏调用saferFree函数,执行类型转换,并使用了取地址操作符,这样就省去函数使用者做类型转换和传递指针的地址:
#define safeFree(p) safeerFree( (void**) &p )
3.3 函数指针
函数指针是持有函数地址的指针。
3.3.1 声明函数指针
类型标识符 (* 指针变量名)( )
void (* foo)( ) void:返回类型 foo:函数指针变量名 参数
使用函数指针时一定要小心,因为C不会检查参数传递是否正确。以下是声明函数指针的一些例子:
int (*f1)(double); //传入 double,返回 int
void (*f2)(char*); //传入 char 指针,没有返回值
double* (*f3)(int,int); //传递两个整数,返回 double 指针
不要把返回指针值的函数和函数指针搞混淆。
int *f4(); //f4是一个函数,返回一个整数指针
int (*f5)(); //f5是一个返回整数的函数指针
int* (*f6)(); //f6是一个返回整数指针的函数指针
3.3.2 使用函数指针
int (*fptr)(int);
int square(int num){
return num*num;
}
...
int n = 5;
fptr = square; // fptr = □ 让指针等于函数的入口地址。
printf("%d squard is %d
",n,fptr(n));
在这种上下文环境中,编译器会忽略取地址符操作,所以你可以写出来,但没必要这么做。
因为fptr是一个函数指针,那么*fptr就是该指针所指向的函数,所以(*fptr)( )就是调用该函数的方式,ANSIC标准允许简写fptr( )。
为函数指针声明一个类型定义会比较方便,类型定义的名字是声明的最后一个元素。
typedef int (*funcptr)(int);
...
funcptr fptr1;
fptr1 = square;
printf("%d squard is %d
",n,fptr1(n));
3.3.3 传递函数指针
传递函数指针很简单,只要把函数指针声明作为函数参数即可。
int add(int num1, int num2) { return num1 + num2; } int sub(int num1, int num2) { return num1 - num2; } typedef int(*fptr)(int, int); int compute(fptr operation, int num1, int num2) { return operation(num1, num2); } printf("%d ", compute(add, 5, 6)); printf("%d ", compute(sub, 5, 6));
3.3.4 返回函数指针
返回函数指针需要把函数的返回类型声明为函数指针。
int add(int num1, int num2) { return num1 + num2; } int sub(int num1, int num2) { return num1 - num2; } typedef int(*fptr)(int, int); fptr select(char opcode) { switch (opcode){ case '+':return add; case '-':return sub; } return 0; } int evaluate(char opcode, int num1, int num2) { fptr operation = select(opcode); //函数指针赋值 return operation(num1, num2); } printf("%d ", evaluate('+', 5, 6)); printf("%d ", evaluate('-', 5, 6));
3.3.5 使用函数指针数组
类型标识符( *指针变量名[ ] ) ( );
int (*operations[128])(int, int)={ NULL }; 这个数组的所有元素都被初始化为NULL
也可以用typedef
来声明这个数组:
typedef int (*operation)(int, int); 如果数组的初始化值是一个语句块,系统会将块中的值赋给连续的数组元素。
operation operations[128]={ NULL };
#include<stdio.h> #include<stdlib.h> int plus(int a, int b){ return(a + b); } int minus(int a, int b){ return(a - b); } int multiplied(int a, int b){ return(a * b); } int divided(int a, int b){ return(a / b); } int percent(int a, int b){ return(a % b); } void main1() { //int(*p)(int a, int b); 函数指针 int(*pn[5])(int a, int b) = { plus,minus, &multiplied ,÷d,percent }; //函数指针数组初始化 int ax = 100; int by = 20; for (int i = 0; i < 5; i++) //下标方式循环数组 printf("%d ", pn[i](ax, by)); //分别调用五种运算输出结果 system("pause"); } void main() //函数指针数组名是二级函数指针 { int(*pn[5])(int a, int b) = { plus,minus, multiplied ,divided,percent }; int ax = 100; int by = 20; //通过二级函数指针的方式遍历函数指针数组 for (int(**p)(int a, int b) = pn; p < pn + 5; p++) //指针循环 { printf("%d ", (*p)(ax, by)); } system("pause"); }
3.3.6 比较函数指针
可以用相等和不等操作符来比较函数指针。
3.3.7 转换函数指针
我们可以将指向某个函数的指针转换为其他类型的指针,不过要谨慎使用,因为运行时系统不会验证函数指针所用的参数是否正确。也可以再转回来,得到的结果和原指针相同,但函数指针的长度不一定相等。
int add(int num1, int num2) { return num1 + num2; } typedef int(*fptrToSing)(int); typedef int(*fptrToInts)(int, int); void main() { fptrToInts fptrFirst = add; fptrToSing fptrSecond = (fptrToSing)fptrFirst; fptrFirst = (fptrToInts)fptrSecond; printf("%d ", fptrFirst(5, 6));
无法保证函数指针和数据指针转换后正常工作。
void*
指针不一定能用在函数指针上。也就是说不应该像下面这样把函数指针赋给void * 指针: void * pv = add;
不过在交换函数指针时,通常会见到如下声明所示的“基本”函数指针类型。这里把fptrBase声明为指向不接受参数也不返回结果的函数的函数指针。
typedef void (*fptrBase)( );