;
此时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) {
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);
View Code
上述运行在内存中运行过程如下图:
图中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;
View Code
数组作为函数参数
注意一点:数组作为函数参数时,整个数组不会被拷贝到子函数的栈空间中,编译器只是创建了一个同名的指针(而不是创建整个数组),将数组首地址拷贝给该特定类型(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;
View Code
方法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]);
View Code
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
View Code
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;
View Code
程序会打印出: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是一个常量指针,指针值不可变。
指针和动态内存
关键词组:内存模型;堆栈溢出(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
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();
View Code
函数返回指针
关键词组:指针是一种类型、函数返回指针的使用场景、被调函数访问主函数变量、返回被调函数的局部变量给主调函数
当我们进行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;
View Code
结果如下:
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);
View Code
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);//打印随机数
View Code
虽然结果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函数的函数体,结果反而回来被调用。
View Code
关于回调函数的详细介绍,后续会单独出一篇。
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
https://www.zhihu.com/question/19801131/answer/17156023?utm_source=weibo&utm_medium=weibo_share&utm_content=share_answer&utm_campaign=share_button