前言

指针对函数功能的贡献极大。它们能够将数据传递给函数,并且允许函数对数据进行修改。我们可以将复杂数据用结构体指针的形式传递给函数和从函数返回。如果指针持有函数的地址,就能动态控制程序的执行流。

在使用函数时,有两种情况指针很有用。首先是将指针传递给函数,这时函数可以修改指针所引用的数据,也可以更高效地传递大块信息。

另一种情况是声明函数指针。本质上,函数表示法就是指针表示法。函数名字经过求值会变成函数的地址,然后函数参数会被传递给函数。我们将会看到,函数指针为控制程序的执行流提供了新的选择。

程序的栈和堆


局部变量也称为自动变量,它们总是分配在栈帧上。


程序栈

程序栈是支持函数执行的内存区域,通常和堆共享。也就是说,它们共享同一块内存区域。程序栈通常占据这块区域的下部,而堆用的则是上部。

程序栈存放栈帧(stack frame),栈帧有时候也称为活跃记录( activation record)或活跃帧(activation frame)。栈帧存放函数参数和局部变量。

void function2() {
Object *var1 = ...;
int var2;
printf("Program Stack Example\n");
}

void function1() {
Object *var3 = ...;
function2();
}

int main() {
int var4;
function1();
}

《深入理解C指针》——指针和函数_数组

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

动态分配的内存来自堆,堆向下"生长"。随着内存的分配和释放,堆中会布满碎片。尽管堆是向下生长的,但这只是个大体方向,​​实际上内存可能在堆上的任意位置分配。​

栈帧的组织

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

  • 返回地址:函数完成后要返回的程序内部地址。
  • 局部数据存储:为局部变量分配的内存。
  • 参数存储:为函数参数分配的内存。
  • 栈指针和基指针:运行时系统用来管理栈的指针。

栈指针通常志向栈顶部。基指针(帧指针)通常存在并指向栈帧内部的地址,比如返回地址,用来协助访问栈帧内部的元素。​​这两个指针都不是C指针,它们是运行时系统管理程序栈的地址。​​如果运行时系统用C实现,这些指针倒真是C指针。

以average函数为例来了解栈帧的创建:

float average(int *arr,int size) {
int sum;
printf("arr: %p\n",&arr);
printf("size: %p\n",&size);
printf("sum: %p\n",&sum);

for(int i = 0; i < size; i++) {
sum += arr[i];
}
return (sum * 1.0f) / size;
}
# 类似输出:
# arr: 0x500
# size: 0x504
# sum: 0x480

《深入理解C指针》——指针和函数_函数指针_02

从原理上说,本例中的栈"向上"生长。不过栈帧的参数和局部变量以及新栈帧被添加到了低内存地址。栈的实际生长方向跟实现相关。

for语句中用到的变量i没有包含在栈帧中。C把块语句当做“微型”函数,会在合适的时机将其推入栈和从栈中弹出。在本例中,块语句在执行时被推到程序栈中average栈帧上面,执行完后又弹出。

​精确的地址可能会变化,不过顺序一般不变​​。这一点很重要,因为它可以解释参数和变量内存分配的相对顺序。在调试指针问题时这一点会很有用。如果你不知道栈帧如何分配,这些地址在你看来也毫无意义。

将栈帧推到程序栈上时,系统可能会耗尽内存,这种情况称为栈溢出,通常会导致程序非正常终止。要牢记每个线程通常都会有自己的程序栈。

通过指针传递和返回数据

用指针传递数据

用指来传递数据的一个主要原因时函数可以修改数据。

void swapWithPointers(int* pnum1,int* pnum2) {
int tmp;
tmp = *pnum1;
*pnum1 = *pnum2;
*pnum2 = tmp;
}

int main() {
int n1 = 5;
int n2 = 10;
swapWithPointers(&n1,&n2);
return 0;
}

用值传递数据

void swapWithPointers(int pnum1,int pnum2) {
int tmp;
tmp = pnum1;
pnum1 = pnum2;
pnum2 = tmp;
}

int main() {
int n1 = 5;
int n2 = 10;
swapWithPointers(n1,n2);
return 0;
}

传递指向常量的指针

传递指向常量的指针是C中常用的技术,效率很高,因为我们只传了数据的地址,能避免某些情况下复制大量内存。不过,如果只是传递指针,数据就能被修改。如果不希望数据被修改,就要传递指向常量的指针。

void passingAddressOfConstants(const int* num1, int* num2) {
*num2 = *num1;
}

int main() {
const int limit = 100; //参数类型一致
int result = 5;
passingAddressOfConstants(&limit, &result);
return 0;
}

修改版本:

void passingAddressOfConstants(const int* num1, int* num2) {
*num1 = 100; //尝试修改第一个变量
*num2 = 200;
}
const int limit = 100;
passingAddressOfConstants(&limit, &limit); //第二个形参和实参类型不匹配

返回指针

从函数返回对象时经常用到以下两种技术:

  • 使用malloc在函数内部分配内存并返回其地址。调用者负责释放返回的内存。
  • 传递一个对象给函数并让函数修改它。这样分配和释放对象的的内存都是调用者的责任。
int* allocateArray(int size,int value) {
int* arr = (int*)malloc(size * sizeof(int));
for(int i=0; i<size; i++) {
arr[i] = value;
}
return arr;
}

int* vector = allocateArray(5,45);
for(int i=0; i<5; i++) {
printf("%d\n", vector[i]);
}

《深入理解C指针》——指针和函数_数组_03

尽管上例可以正确工作,但从函数返回指针时可能存在几个潜在的问题,包括:

  • 返回未初始化的指针;
  • 返回指向无效地址的指针;
  • 返回局部变量的指针;
  • 返回指针但是没有释放内存。
int* vector = allocateArray(5,45);
...
free(vector); //必须释放内存

局部数据指针

int* allocateArray(int size,int value) {
int* arr[size];
for(int i=0; i<size; i++) {
arr[i] = value;
}
return arr;
}

不幸的是,一旦函数返回,返回的数组地址也就无效了,因为函数的栈帧从栈中弹出了。尽管每个数组元素仍然可能包含45,但如果调用另一个函数,就可能覆写这些值。下面的代码段对此做了演示,重复调用printf 函数导致数组损坏:

int* vector = allocateArray(5,45);
for(int i=0; i<5; i++) {
printf("%d\n", vector[i]);
}

《深入理解C指针》——指针和函数_数据_04

传递空指针

将指针传递给函数时,使用之前先判断它是否为空是个好习惯。

int* allocateArray(int *arr, int size, int value) {
if(arr != NULL) {
for(int i=0; i<size; i++) {
arr[i] = value;
}
)
return arr;
)
int* vector = (int*)malloc(5 * sizeof(int));
allocateArray (vector.5,45);

传递指针的指针(重要)

​将指针传递给函数时,传递的是值。如果我们想修改原指针而不是指针的副本,就需要传递指针的指针。​

在下例中,我们传递了一个整数数组的指针,为该数组分配内存并将其初始化。函数会用第一天参数返回分配的内存。在函数中,我们先分配内存,然后初始化。所分配的内存地址应该被赋给一个整数指针。​​为了在调用函数中修改这个指针,我们需要传入指针的地址。​

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,4,45);

allocateArray的第一个参数以整数指针的指针的形式传递。当我们调用这个函数时,需要传递这种类型的值。这是通过传递vector地址做到的。malloc返回的地址被赋给arr。解引整数指针的指针得到的时整数指针。因为这是vector的地址,所以我们修改了vector。

《深入理解C指针》——指针和函数_c语言_05

注:​​因为vector本身就是int *,如果想传递值的话参数本身就应该时int *,要想传递指针就应该时int **。​

错误版本:

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, 45);
printf("%p\n", vector);

运行后会看到程序打印出0x0(已运行的确是00000),​​因为将vector传递给函数时,它的值被复制到了参数arr中,修改arr对vector没有影响。​​当函数返回后,没有将存储在arr中的值复制到vector中。

注:这里由内存泄漏,因为我们无法再访问地址600处的内存块了。

《深入理解C指针》——指针和函数_函数指针_06

实现自己的free函数

由于free函数存在一些问题,因而某些程序员创建了自己的free 函数。free 函数不会检查传入的指针是否是NULL,也不会在返回前把指针置为NULL。释放指针之后将其置为NULL是个好习惯。

void safeFree(void **pp) {
if(pp != NULL && pp != NULL) {
free(*pp);
*pp = NULL;
}
}

saferFree函数调用实际释放内存的free函数,前者的参数声明为void指针的指针。使用指针的指针允许我们修改传入的指针,而使用void类型则可以传入所有类型的指针。不过,如果调用这个函数时没有显式地把指针类型转换为void会产生警告,执行显式转换就不会有警告。

下面这个safeFree宏调用saferFree函数,执行类型转换,并使用了取地址操作符,这样就省去了函数使用者做类型转换和传递指针的地址:

#define safeFree(p) safeFree((void**)&p);

使用示例:

int main() {
int *pi;
pi =(int*) malloc(sizeof(int));
*pi = 5;
printf ("Before: %p\n", pi);
safeFree(pi);
printf("After: %p\n", pi);
safeFree(pi);
return (EXIT_SUCCESS);
)

函数指针

人们使用函数指针的一个顾虑是这种做法可能会导致程序运行变慢,处理器可能无法配合流水线做分支预测。分支预测是处理器用来推测哪块代码会被执行的技术。

声明函数指针

void (*foo)();

注:使用函数指针一定要小心,因为C不会检查参数传递是否正确。

使用函数指针

int (*fptr1)(int);

int square(int num) {
return num*num;
}

int n = 5;
fptr1 = square;
printf("%d squared is %d\n",n ,fptr1(n));

传递函数指针(重要)

传递函数指针很简单,只要把函数指针声明(注意需要使用typedef,要不然报错)作为函数参数即可。

int add(int num1, int num2) {
return num1 + num2;
}

int sub(int num1, int num2) {
return num1 - num2;
}

typedef int (*fptrOperation) (int, int); //重要

int compute(fptrOperation operation, int num1, int num2) {
return operation(num1, num2);
}

//这功能牛
printf("%d\n", compute(add, 5, 6)); //11
printf("%d\n", compute(sub, 5, 6)); //-1
return 0;

返回函数指针

返回函数指针需要把函数的返回类型声明为函数指针。

fptrOperation select(char opcode) {
switch(opcode) {
case '+': return add;
case '-': return sub;
}
}

int evaluate(char opcode, int num1, int num2) {
fptrOperation operation = select(opcode);
return operation(num1, num2);
}

printf("%d\n", evaluate('+', 5, 6)); //11
printf("%d\n", evaluate('-', 5, 6)); //-1

使用函数指针数组

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

typedef int (*operation)(int, int);
operation operations[128] = {NULL};
//也可以不用typedef来声明这个数组,如下:
int (*operations[128])(int, int) = {NULL}; //这个数组的目的是可以用一个字符索引选择对应的函数来执行(注意前面的int表明数组类型)

如果存在*字符就表示乘法函数,我们可以用字符作为索引​​是因为字符字面量其实是整数​​,128个元素对应前128个ASCII字符。

void initalizeOperationsArray() {
operations['+'] = add;
operations['-'] = sub;
}

int evaluateArray(char opcode, int num1, int num2) {
fptrOperation operation;
operation = operations[opcode];
return operation(num1, num2);
}

initalizeOperationsArray();
printf("%d\n", evaluateArray('+', 5, 6)); //11
printf("%d\n", evaluateArray('-', 5, 6)); //-1

比较函数指针

我们可以用相等和不等操作符来比较函数指针。

fptroperation fptr1 = add;

if(fptr1 == add) {
printf("fptr1 points to add function\n");
} else {
printf("fptr1 does not point to add function\n");
}

比较函数指针

我们可以将指向某个函数的指针转换为其他类型学的指针,不过要谨慎使用,因为运行时系统不会验证函数指针所用的参数是否正确。

也可以把一种函数指针转换为另一种再转换回来,得到的结果和原指针相同,但函数指针的长度不一定相等。

typedef int (*fptrToSingleInt)(int);
typedef int (*fptrToTwoInts)(int, int);
int add(int, int);

fptrToTwoInts fptrFirst = add;
fptrToSingleInt fptrSecond = (fptrToSingleInt) fptrFirst;
fptrFirst = (fptrToTwoInts) fptrSecond;
printf("%d\n", fptrFirst(5, 6)); //11

void*指针不一定能用在函数指针上,也就是说我们不应该像下面这样把函数指针赋给void*指针:

void* pv = add;

不过在交换函数指针时,通常会见到如下声明所示的"基本"函数指针类型。这里​​把fptrBase声明为指向不接受参数也不返回结果的函数的函数指针。​

typedf void (*fptrBase)();

这类基本指针用做占位符,用来交换函数指针的值。

fptrBase basePointer;
fptrFirst = add;
basePointer = (fptrToSingleInt) fptrFirst;
fptrFirst = (fptrTwoInts)basePointer;
printf("%d\n", fptrFirst(5, 6));

小结

研究了栈、堆和栈帧,以及函数指针等。