4小时彻底掌握C指针
本文依据已故的印度程序员Harsha Suryanarayana图文讲解整理而来。
B站视频链接:https://www.bilibili.com/video/BV1bo4y1Z7xf
指针的基本介绍
关键词组:内存图示(体系架构)、数据所在内存区域(文本区、常量区、堆区、栈区),不同数据类型的内存分配情况、每行代码执行时在内存中的图示,指针,指针值&所占空间大小
不同数据类型或变量在数据在内存中如何存储?
首先内存是一块一块连续的地址,以字节为单位,一个字节为一个内存单元形如下图所示。
内存根据实际情况分为不同的区段,当在代码中声明一个变量时,知晓其所在区段,此处我们在Main函数中声明一个int a;其会在栈区分配一块内存给变量a.
不同编译器在对于不同的数据类型分配的字节数是不一样的。一般典型的编译器对于int char float数据类型分配情况如图所示,分配的字节数通过sizeof(数据类型)得到。
代码行语句在内存中的运行情况:
int a;
char c;
a=5;
a++;
当在Main函数中声明int a时,程序运行时首先会被分配在栈区,然后编译器根据其数据类型为整型随机分配4字节的地址为204~207,
同样的,当声明char类型变量c时,分配字节大小为1,起始地址为209的内存空间给变量c.
执行到a=5语句时,会将数据5(二进制00000000 00000000 00000000 00000110)以204为起始地址连续写入4字节到内存中,当然此处要注意数据写入时的大小端情况。
同样的,当执行到a++语句时,会先找到变量a在内存中的起始地址,然后a自增,将数据6以二进制的方式写入到204~207中,在该语句其实就是找地址,修改(操作)该地址下的变量值。正好,C语言指针提供的这样的功能。
指针变量:存储其他变量地址的变量。指针是强类型的,一定是指向特定的数据类型变量(指针值为该变量的起始地址),比如下图中的int *p其指向整型变量。
p为地址,*p为解引用,即得到该地址下的值。
以下面语句为例:
int a;
int *p;//声明整型指针p
p=&a;//&符号位取地址符,p指向了整型变量a
a=5;
print p or &a //取得了整型a的地址
*p=8;//通过指针修改地址下的值。
当执行语句int *p时,首先会在栈区随机分配8字节(64位系统)固定大小的字节数用于存储变量a的地址值204.指针所占内存空间的大小与指针所指向的数据类型没有关系,指针始终是指向特定类型(int,char ,etc)的数据的首地址,即指针值为数据的首地址;
而跟系统的寻址能力有关。32位机器为4字节,64位为8字节。
指针代码示例
关键词组:野指针、变量初始化、指针修改指向单元的值,指针运算
野指针(wild Pointer):指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。野指针不是NULL空指针。
成因一般有一下几点:
指针变量未初始化:
指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针。
在Debug模式下,VC++编译器会把未初始化的栈内存上的指针全部填成 0xcccccccc ,当字符串看就是 “烫烫烫烫……”;会把未初始化的堆内存上的指针全部填成 0xcdcdcdcd,当字符串看就是 “屯屯屯屯……”。把未初始化的指针自动初始化为0xcccccccc或0xcdcdcdcd,而不是就让取随机值,那是为了方便我们调试程序,使我们能够一眼就能确定我们使用了未初始化的野指针。在Release模式下,编译器则会将指针赋随机值,它会乱指一气。所以,指针变量在创建时应当被初始化,要么将其设置为NULL,要么让它指向合法的内存
#include<stdio.h> int main() { int a; int* p; //p = &a; printf("%d\n", p); //printf("%d\n", *p); return 0; }
此时p就是一个野指针。
指针释放之后未置空:
有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。
int num = 6; int* p = # cout << *p << endl; free(p);//p所指向的堆内存单元已经释放 cout << *p << endl; /// p是野指针
指针操作超越变量作用域:
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
class A { public: void Func(void){ cout << “Func of class A” << endl; } }; class B { public: A *p; void Test(void) { A a; p = &a; // 注意a的生命期 ,只在这个函数Test中,而不是整个class B } void Test1() { p->Func(); // p 是“野指针”,Test函数指向完毕后,a在栈上的地址空间已经被清除掉 } };
定义变量要初始化,不然会产生随机值。
通过指针修改指针指向的地址处的值。
指针运算
当指针指向了具体类型的数据后,可以对指针p进行运算,比如p++;p--。
但是不能对指针运算过后的地址解引用取值( *(p+1) ),一般会得到随机数。
指针的类型,算术运算,void指针
关键词组:void指针、类型转换
前面我们讲到指针是强类型,意味着需要用特定类型的指针变量来存放特定类型变量的地址。。如果我们想要一种通用类型的指针变量来存储所有类型变量,此时可以使用void指针。
但是在将void指针进行类型转换成特定类型指针的时候,要特别的注意。不同的数据类型有不同的存储空间大小,可能会存在数据截断的情况。
#include<stdio.h> /* * 知识点 第一点: 指针是强类型的,意味着: 需要用特定类型的指针变量来存放特定类型变量的地址。 int *-->int char*-->char 那为什么不能用一个通用类型的指针变量来存储所有类型变量呢?-->void*的提出 第二点: 可以使用*来解引用,访问和修改这些地址对应的值。此处涉及到通用类型变量指针在解引用时候的情况处理 不同的数据类型有不同的存储空间大小 一般 int 4 bytes; float 4 bytes char 1 byte */ void TypeCaseTest() { int a = 1025; int* p; p = &a; printf("size of integer is %d bytes\n", sizeof(int)); printf("address =%d, value=%d\n", p, *p); printf("address =%d,value=%d\n", p + 1, *(p + 1)); char* p0; p0 = (char*)p;//type casting printf("size of char is %d bytes\n", sizeof(char)); printf("address =%d,value=%d\n", p0, *p0); printf("address =%d,value=%d\n", p0 + 1, *(p0 + 1)); //1025=00000000 00000000 00000100 00000001 } //指针类型、类型转换、指针运算内容 int main() { TypeCaseTest(); return 0; }
我们看到*p0值为1,即00000001,因为它是char指针解引用。系统只能取到1个字节的数据。*(p0+1)为00000100
在使用void指针进行解引用(*p)或指针运算(p++)的时候,要特别的注意。可能存在报错的情况。
int a = 1025; int* p; p = &a; printf("size of interger is %d bytes\n",sizeof(int)); printf("address =%d,value=%d", p, *p); //Void pointer--Genric pointer void* p0; p0 = p;//此处不用进行强制转换p0=(int*)p printf("address=%d\n", p0); printf("address=%d\n", p0 + 1);//不知道p0具体error表达式必须包含指向 类 的指针类型,因为不知道P0指针指向的具体数据类型,所以没法其指针+1,不知道具体类型,有可能地址+1,+4 printf("value=%d\n", *p0);//error 表达式必须包含指向 类 的指针类型,因为不知道P0指针指向的具体数据类型,所以没法对其解引用。
指向指针的指针
指针的套娃。
理解几个形式:
int x=6;
int* p=&x;
int ** q=&p;//q为一个指向指针的指针
int ***r=&q;//r为一个指针的指针的指针
在对套娃的指针进行解引用的时候,一层一层通过*解引用即可。
int x = 5; int* p; p = &x; *p = 6;//修改值 int** q; q = &p; int*** r; r = &q; printf("%d\n", *p); printf("%d\n", *q); printf("%d\n", **q); printf("%d\n", **r); printf("%d\n", ***r); ***r = 10; printf("x=%d\n", x); **q = *p + 2; printf("x=%d\n", x);
函数传值VS传引用
关键词组:内存模型图、传值与传引用区别
指针一个典型应用是作为函数参数使用。我们先以函数传值&传引用为例讲述两者区别。
#include<stdio.h> void Increment(int a) { a = a + 1;//x=x+1; printf("address of variable a in increment =%d\n", &a); } void IncrementByReference(int* p) { *p = *p + 1; } int main() { int a = 10; Increment(a); printf("address of variable a in main =%d\n", &a); printf("value of a= %d\n", a);
上述运行在内存中运行过程如下图:
图中Code,static/Gloal和Stack空间都是固定大小,Heap空间是不固定的可以自行去分配和释放。
执行主函数时,执行int a=10后会在main函数的栈区(stack frame)分配内存给局部变量(起始地址为300);
执行到Increment(a)后系统产生中断,转而执行子程序Increment,内存分配另外的栈空间给此函数,其中会将主程序实参a的值拷贝给形参a(tips:两者地址不一样),如图红框处的栈空间。当Increment执行完毕后其栈空间内容清除掉,随后回到主函数往下执行打印函数。
a的值未改变,仍然为10。
传引用修改值:
void IncrementByReference(int* p)//此处int* p是指针参数的声明形式 { *p = *p + 1; } int main() { int a = 10; IncrementByReference(&a); }
结果为11.
内存运行情况分析:
实参将a地址(address=308)传给指针p后,在子函数内部指针解引用修改308地址处的值,该地址空间始终存在。
最后a=11。
对比传值跟传引用,会发现函数传值,内存会额外分配多的空间(如上例中increment的栈区空间都是),而传引用只会分配4或8字节的P指针的空间,如果参数数据类型更加复杂,increment栈区空间局部变量所占空间更大。
指针和数组
注意几点:
1、指针在进行算术运行时(如增加1时,p+1),会产生野指针(因为p+1,即p+sizeof(特定类型),该地址下的值不知道是什么。。)
2、数组可以在数组长度内进行算术运行
int A[5]
print A+1;A+2;...A+4
3.数组索引i处的地址和值的表示:
address:&A[I] or (A+i)
value: A[i] or *(A+i)
#include<stdio.h> int main() { int A[] = { 2,4,5,8,1 }; int i; int* p = A; p++; //A++;//报错:表达式必须是可修改的左值(A此时是数组A的地址,是一个常量值,不能进行算术运算) printf("%d\n", A); printf("%d\n", &A[0]); printf("%d\n", A[0]); printf("%d\n", *A); for (int i = 0; i < 5; i++) { printf("address =%d\n",& A[i]); printf("address =%d\n", A+i); printf("value =%d\n", A[i]); printf("value =%d\n", *(A + i)); } return 0; }
数组作为函数参数
注意一点:数组作为函数参数时,整个数组不会被拷贝到子函数的栈空间中,编译器只是创建了一个同名的指针(而不是创建整个数组),将数组首地址拷贝给该特定类型(int,char etc)的指针。
如以下求和的示例:
注意:main函数栈帧与SOE栈帧中A的不同。main中A为数组A(20字节),SOE中A为同名的整型指针(4字节)
int main() { int A[] = { 1,2,3,4,5 }; //第一种方法求解总和 { int size = sizeof(A) / sizeof(A[0]); int total = SumOfElement(A, size); } //第二种得到的结果不对, int total = SumOfElement1(A); printf("Sum of elements=%d\n", total); printf("Main-size of A=%d,size of A[0]=%d\n", sizeof(A), sizeof(A[0])); return 0; }
int SumOfElement(int A[],int size)//int *A or intA[] it's the same(编译器会隐式转换intA[] 为int *A { int sum = 0; for (int i = 0; i < size; i++) { sum += A[i]; } return sum; } int SumOfElement1(int A[]) { int sum = 0; int size = sizeof(A) / sizeof(A[0]); // sizeof(A)为4,因为编译器此处将并不会拷贝实参的整个数组值,而是数组指针(其实深入思考,如果数组很大,直接拷贝数组容量大小的数据也会浪费空间),int A[]等价于int *A,所以sizeof(A)为4 printf("SOE-size of A=%d,sizeof A[0]=%d\n", sizeof(A), sizeof(A[0])); for (int i = 0; i < size; i++) { sum += A[i]; } return sum; }
方法1 result=15; 方法2 result=1;//结果错误
void Double(int* A, int size) { for (int i = 0; i < size; i++) { A[i] = 2 * A[i]; } } int main() { int A[] = { 1,2,3,4,5 }; { int size = sizeof(A) / sizeof(A[0]); Double(A, size); for (int i = 0; i < size; i++) { printf("%d ", A[i]); } }
result: 2 4 6 8 10
指针和字符数组
关键词组:字符数组的几种声明;'\0';指针作为函数参数;常量指针(指针指向的内容只读,不能写);指针常量;
如何存储字符串,以存储字符串"JOHN"为例
C语言中没有字符串类型的概念,只有字符数组。此字符串长度为4,sizeof(C)字节长度为5,用字符数组存储数组长度要大于等于len+1;此处即5,char C[5],char[20]都是合法的。
C语言的基本数据类型中并没有字符串类型,在使用的过程中通过指针或字符数组来实现。但是两者在内存中的位置还是有差别的。
char str1[] = "abcd";
char *str2 = "abcd";
于str1在内存中的存放方式是{‘a’,‘b’,‘c’,‘d’,’\0’},是以字符数组的形式存放在内存中,在函数定义时存放在栈区,函数结束就释放。
而str2存放在字符常量区,即全局区,当程序结束才释放
字符数组的几种声明方式:
char C[5];//逐一赋值 C[0] = 'J'; C[1] = 'O'; C[2] = 'H'; C[3] = 'N'; C[4] = '\0';
int main() { char C[5]; C[0] = 'J'; C[1] = 'O'; C[2] = 'H'; C[3] = 'N'; C[4] = '\0'; int len = strlen(C); printf("%s\r\n", C);//JOHN printf("length is %d\r\n", len);//4 }
char C[5] = "JOHN";//第二种写法:可以不用写结束符,但是字符数组空间要>=len+1
char C[5] = "JOHN";//可以不用写结束符,但是字符数组空间要>=len+1 printf("size of bytes =%d\n", sizeof(C)); int len = strlen(C); printf("length =%d\n", len)
result: size of bytes =5 length =4
char AnotherC[5] = { 'J','O','H','N','\0' };//第三种:另一种声明字符数组的写法,需要显示写上结束符\0
字符数组与字符指针相关操作:
char C1[6] = "HELLO"; char* C2; C2 = C1; //print C2[1];//e C2[0] = 'A';//C2[i] is *(C2+i) C1=C2;//报错 E0137 表达式必须是可修改的左值 C1=C1+1;X C1是一个常量
C2++是对的
字符指针(假设p)作为函数参数时,会在该函数栈帧中存储p,且p=实参地址值
void print(char* C) { int i = 0; while (C[i]!='\0')//C[i] equals *(C+i),so 也可以写成*(C+i) { printf("%c", C[i]); i++; } //下面的也是对的 //while (*C!='\0') //{ // printf("%c", *C); // C++; //} printf("\n"); } int main() { char C[20] = "HELLO"; print(C); return 0; }
程序会打印出:HELLO
我们也可以在print函数中对字符数组值进行修改
C[0] = 'A';//c[i] 等于*(c+i) while (*C!='\0') { printf("%c", *C); C++; } printf("\n");
结果为:AELLO
如果我们不想在print函数的时候数组值被修改,或者说函数是只读的,可以在指针参数前加上const关键字这样传入函数
void print(const char* C) { ... }
const char * c :表示c指针所指向的内存内容不能改变,是只读的。
char * const c:表示c是一个常量指针,指针值不可变。
指针和动态内存
栈vs堆
关键词组:内存模型;堆栈溢出(stack overflow);堆的引入,两者区别;
如下图:
内存被分为代码区、静态常量区、栈区、堆区。
static变量以及全局变量会分配在静态常量区,会随着程序一直存在,程序结束,对应空间就进行释放了。
函数以及函数的局部变量存储在栈区;如上图中main函数以及其下 的a,b;sos里面的x,y,z;sq里面的r变量,栈空间在程序刚开始时就已经分配好了,对于 x86 和 x64 计算机,默认堆栈大小为 1 MB。当程序一直嵌套调用,或者递归调用耗尽栈空间时,会出现stack overflow栈溢出。另外,栈空间都是固定大小的,如果我们想根据传入的参数n值来动态的分配大容量(超过1M)的数组时,很明显此时栈空间已经不符合我们的要求了。此时可以通过堆来存储大容量的数组。
堆空间时自己申请,自己释放的。若程序员不释放的话,程序结束时可能由OS回收,但其与数据结构中的堆是两回事,分配方式倒是类似于数据结构的链表。
堆空间一般是很大的一段地址空间。
C语言中堆空间申请通过malloc申请,free进行释放。C++中配套使用new delete
程序执行片段内存分析:
#include<stdio.h> #include<stdlib.h> int main() { int a; int* p; p = (int*)malloc(sizeof(int)); *p = 10;
free(p);
p = (int*)malloc(sizeof(int));
*p=20
}
首先程序在执行main函数,在栈区申请一段空间存储main函数栈区,在该区域还有局部变量a,p;
执行malloc语句申请一段4字节堆内存,堆内存的地址由指针p执行,即p赋值为该堆空间地址。
此时地址200处还未填值,通过*操作解引用堆空间内容为10.
赋值完成后,执行free,释放p指向的堆内存。
执行malloc重新申请一段空间,p指针指向这段还未赋值的空间。
*p解引用将20填入该堆空间。
当然也可以申请一段连续的空间
p=(int*)malloc(20*sizeof(int));
//p[0],p[1],p[2] or *p,*(p+1) becase p[i] equals *(p+i)
malloc calloc realloc free
malloc函数用于在内存的动态存储区中分配一个长度为size的连续空间。此函数的返回值是分配区域的起始地址,其不初始化时内容为为随机值。
函数原型:void* malloc (size_t size);
calloc() 函数用来动态地分配内存空间并初始化为 0,
函数原型:void* calloc (size_t num, size_t size);
realloc函数用来扩大已经开辟好的堆空间。
void *realloc(*mem_addr,unsigned int newsize)
含义是:(数据类型*)realloc(要扩大内存的指针名,新的内存大小)
这里有2种情况:
1、够开辟新的newsize,即mem_addr开始的空闲内存不小于newsize,则返回mem_addr。
2、不够开辟的newsize,即mem_addr开始的空闲内存小于newsize,则会换一个新的地方重新开辟newsize大小的内存,并将mem_addr处开始的数据自动拷贝到新的地址,mem_addr开始的原来的内存也自动释放掉,不用手动free。返回新的首地址。
int main() { int n; printf("enter size of array\r\n"); scanf_s("%d", &n); int* A = (int*)malloc(n * sizeof(int));//malloc不初始化的时候为随机值 //int *A = (int*)calloc(n, sizeof(int));//calloc函数不初始化的时候都为0 for (int i = 0; i < n; i++) { A[i] = i + 1; } //free(A); int* B = (int*)realloc(A, n * sizeof(int)); //int* B = (int*)realloc(A, 0);//equivalent to free(A) //int* B = (int*)realloc(NULL, n * sizeof(int));//equivalent to malloc(n*sizeof(int)) printf("prev block address =%d,new address=%d\n", A, B); for (int i = 0; i < 2*n; i++) { printf("%d ", A[i]); } return 0; }
输入5,结果为:
enter size of array 5 prev block address =18497888,new address=18497888 1 2 3 4 5 -33686019 -636223161 35591 18528192 18501808
说明realloc够开辟新的newsize,直接在A指针原有的数据后面追加了newsize长度的内存空间。
内存泄漏
内存泄漏是指不当地使用动态内存或者内存的堆区,泄漏的内存在一段时间增长。内存泄漏总是因为堆中未使用和未引用的内存块发生的。栈空间会自动清除,顶多出现stackoverflow堆栈溢出。
代码变量的内存分布情况如上图所示:
关键点解释一下:
在TestMemoryOverflow函数堆栈中,存在指针C,指向堆中100字节的数组首地址,函数执行完毕后,C指针空间清除,堆中这100字节未使用,while一直执行,程序就内存泄漏了。【会看到随着程序运行,任务管理器中该程序使用内存一直变大】
另外两个函数中的C变量一个是栈分配空间,自动清除;另一个是堆空间,但是会free手动释放。【会看到随着程序运行,任务管理器中该程序使用内存一直维持在一个稳定值】
// ConsoleApplication2.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include<stdio.h> #include<stdlib.h> int GlobalCount = 0; void NormalMemoryAlloc() { char C[100]; printf("normal memory\r\n"); } void TestMemoryOverflow() { char* C =(char*) malloc(100 * sizeof(char)); printf("MemoryOverflow\r\n"); } void NoMemoryOverflow() { char* C = (char*)malloc(100 * sizeof(char)); printf("MemoryOverflow\r\n"); free(C); } int main() { while (true) { GlobalCount++; NormalMemoryAlloc(); TestMemoryOverflow(); NoMemoryOverflow(); } }
函数返回指针
关键词组:指针是一种类型、函数返回指针的使用场景、被调函数访问主函数变量、返回被调函数的局部变量给主调函数
当我们进行Add操作时,如果传递值进行调用,可以查看在传递参数时是进行值拷贝,形参实参地址分别在不同栈帧空间下,如下:
int Add(int a, int b) { printf("address of a in Add =%d\n", &a); int c = a + b; return c; } int main() { int a = 10, b = 20; printf("address of a in main =%d\n", &a); //call by value int c = Add(a, b); //value in a of main is cpoied to a of add; //value in b of main is cpoied to b of add; }
结果如下:
address of a in main =5962472 address of a in Add =5962208
引用传递时,结果如下:
int AddByReference(int *a, int *b) { printf("address of a in AddByReference =%d\n", a); int c = *a + *b; return c; } int main() { int a = 10, b = 20; printf("address of a in main =%d\n", &a); int c = AddByReference(&a, &b); printf("Sum=%d\n", c); }
address of a in main =8125180 address of a in AddByReference =8125180 Sum=30
如果被调函数返回指针呢?
//此情况下主调函数无法访问到被调函数的局部变量(因为该栈空间在函数调用结束后就清除了) int* AddByRefReturnPointer(int *a, int *b) { int c = *a + *b; return &c; } int main() { int a = 10, b = 20; printf("sum =%d\n", *res);//打印随机数 }
虽然结果sum=30是正确的,但是当我们在打印前调用PrintHelloWorld函数时,sum为随机值
分析其内存分配情况:
在执行AddByRefReturnPointer时,会在栈上分配AddByRefReturnPointer栈帧空间,里面有4字节的指针a,b,局部变量c(假设地址为144,值则为30)。
当函数指向完毕时,该栈帧空间被清除掉,虽然c的地址以返回值的形式返回到主函数res指针,但是该指针所值的内存空间已经被清除掉。
当执行PrintHelloWorld函数时,该地址值可能被重写,可能未分配值是随机数。
void PrintHelloWorld() { printf("hello world\n"); } int* res=AddByRefReturnPointer(&a,&b); PrintHelloWorld(); printf("sum =%d\n", *res);//打印随机数
hello world sum =-858993460
如果想要正常返回结果呢?
//次此情况下,主调函数可以访问到被调函数的局部变量(因为其 int* AddByRefReturnPointer1(int *a, int *b) { int* c = (int*)malloc(sizeof(int)); *c= *a + *b; return c;//函数执行完毕指针在该堆栈上的空间(4字节)释放了,但是堆上空间没有,同时堆上该空间的首地址作为返回值返回到了主函数 }
分析其内存分配情况:
在执行AddByRefReturnPointer1时,会在栈上分配AddByRefReturnPointer1栈帧空间,里面有4字节的指针a,b,局部变量指针c(假设地址为144,则指向的地址的值为30)。
当函数指向完毕时,该栈帧空间被清除掉,c的地址以返回值的形式返回到主函数res指针,其指向的堆地址空间也存在,故主函数可以对其进行访问
当执行PrintHelloWorld函数时,该地址值也不会存在问题。
被调函数执行时,主调函数能够确保还在栈内存中(依据栈的数据结构特点),所以被调函数此时还能访问主调函数局部变量(main函数中的变量地址对Add来讲是可以访问的)
但是如果我们尝试返回一个被调函数的局部变量给主调函数时呢,就会出现问题。(因为被调函数的栈空间已经被释放了)
一层一层递进,所以也就有了函数的入口函数main函数。
函数指针
关键词组:函数指针的引出(汇编中jump,函数指针指向函数的入口地址entrypoint)、定义以及使用
函数指针是指向函数的指针变量。
通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。
函数指针可以像一般函数一样,用于调用函数、传递参数(回调函数)。
函数指针变量的声明:
typedef int (*fun_ptr)(int,int); // 声明一个指向int,int型的参数、int返回值的函数指针类型
#include<stdio.h> void PrintHello(const char* name) { printf("hello %s\n", name); } int Add(int a, int b) { return a + b; } int main() { //两种书写方式: int c; int (*p)(int, int);//声明函数指针p,该指针所指向的函数返回值为int,形参为(int,int) //p = &Add;//指针建立指向 //c = (*p)(2, 3);//p指针解引用获得函数,然后传入参数执行函数 p = Add;//函数名也是函数的首地址,没毛病 c = p(2, 3);//p指针解引用获得函数,然后传入参数执行函数 printf("%d\n", c); void (*ptr)(const char*); ptr = &PrintHello; ptr("jack"); }
注意:int (*p)(int, int)的括号,没有括号,编译器将假定这p
是一个普通的函数名,形参为int,int,并返回一个指向整数的指针。
上面部分的函数指针主要用户函数调用的功能;下面讲解函数指针作为函数参数实现函数回调。
函数指针的使用(回调函数)
关键词组:回调函数的定义、回调函数三部分、回调函数使用场景(QuickSort)、事件
维基百科定义:
在计算机程序设计中,回调函数,或简称回调(Callback),是指通过函数参数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。
知乎:桥头堡 回答
什么是回调函数?
我们绕点远路来回答这个问题。
编程分为两类:系统编程(system programming)和应用编程(application programming)。所谓系统编程,简单来说,就是编写库;而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用。系统程序员会给自己写的库留下一些接口,即API(application programming interface,应用编程接口),以供应用程序员使用。所以在抽象层的图示里,库位于应用的底下。
当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。
打个比方,有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)。如下图所示(图片来源:维基百科):
#include<stdio.h> void A()//A是回调函数,回调函数就是用来给别人调用的函数 { printf("Hello"); } void B(void(*ptr)())//function pointer as argument { ptr();//call back function that "ptr" points to } int main() { /*void(*p)() = A; B(p);*/ //等价于下面的 B(A);//A是回调函数(我理解回调函数的调用过程就是把该函数指针传入主调函数,然后主调函数B通过函数指针来回调它, //回调就是它作为B的参数,传入实参后进入B函数的函数体,结果反而回来被调用。 }
关于回调函数的详细介绍,后续会单独出一篇。
参考:
https://www.cnblogs.com/kira2will/p/3477511.html#:~:text=%E5%9C%A8%20%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%20%E4%B8%AD%EF%BC%8C%20%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0%20%EF%BC%8C%E6%88%96%E7%AE%80%E7%A7%B0%20%E5%9B%9E%E8%B0%83%20%EF%BC%88Callback%EF%BC%89%EF%BC%8C%E6%98%AF%E6%8C%87%E9%80%9A%E8%BF%87%20%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0,%E5%BC%95%E7%94%A8%20%E3%80%82%20%E8%BF%99%E4%B8%80%E8%AE%BE%E8%AE%A1%E5%85%81%E8%AE%B8%E4%BA%86%20%E5%BA%95%E5%B1%82%20%E4%BB%A3%E7%A0%81%E8%B0%83%E7%94%A8%E5%9C%A8%E9%AB%98%E5%B1%82%E5%AE%9A%E4%B9%89%E7%9A%84%20%E5%AD%90%E7%A8%8B%E5%BA%8F%20%E3%80%82%20%E7%BB%B4%E5%9F%BA%E7%99%BE%E7%A7%91%E9%93%BE%E6%8E%A5%EF%BC%9Ahttp%3A%2F%2Fzh.wikipedia.org%2Fzh-cn%2F%25E5%259B%259E%25E8%25B0%2583%25E5%2587%25BD%25E6%2595%25B0