前言指针,无疑是许多初学者的当头一棒。
由于指针的抽象层次低,直接与内存打交道,使得指针迫使你面对“数据在内存中具体放在哪里”这个底层问题,这对于初学者或习惯了高级抽象的程序员来说很不直观。再加上指针的一些声明语法有时会显得晦涩难懂,尤其是当指针与数组、函数结合的时候更是规则繁多。
为帮助对指针感到头疼的初学者,或者其他从其他语言转到C的程序员更加清楚明了的掌握指针,于是这篇文章诞生。
那么,什么是指针呢?
我们假如将计算机上的内存空间想象成一栋宿舍楼,每层楼都有若干宿舍。
我们规定:
①每个宿舍都有一个唯一的地址编号,
②每个宿舍只能通过编号找到。
在现实生活中,我们将类似的编号叫”地址“,而在C语言中,这个编号换了个洋气的名字——“指针”。
综上,C语言中的指针就是计算机内存地址的别称,即指针==地址。
一、指针基础1.指针变量与取地址符&观察下面的几行代码,猜猜x y z与 a b c哪组可能是指针变量?
代码语言:javascript代码运行次数:0运行复制int x=1;
int * a=&x;
char y='y';
char *b=&y;
double z=3.1415926;
double *c=&z;指针变量:指针变量与普通变量区别就是多了‘ * ’。
如int x;这里的x是普通整形变量。而int * x;这里的x就是指针变量。
那么指针变量是干什么的呢?
答案是储存指针(即地址)。
‘&’:这是取地址操作符,它的作用是取出存储在计算机中变量的地址。
综上,如何创建并初始化指针变量就呼之欲出了:
代码语言:javascript代码运行次数:0运行复制int x = 1;//定义一个普通整形变量x;
int * a = &x;//将x的地址取出,然后赋值给指针变量a;于是乎,我们就创建好了一个指针变量a,它里面存储着x在计算机中的地址。
2.指针变量类型和解引用操作符‘ * ’代码语言:javascript代码运行次数:0运行复制int * a;这⾥a左边写的是 int* , * 是在说明p是指针变量,⽽前⾯的 int 是在说明p指向的是整型(int) 类型的变量。
那么如果有一个char类型的变量y,那它的地址应该放在什么类型的指针变量中呢?
是的,我们应该放在char*类型的指针变量中。
如下
代码语言:javascript代码运行次数:0运行复制char y = 'y';
char *b = &y;综上我们可以得出一个结论:指针变量的类型是与它想要存储的变量类型一致的,只不过指针变量多了一个‘ * ’。
那么这个‘ * ’是什么呢?
在现实生活中,我们只要知道一个人的家庭住址,那么在这个地址就能找到这个人。
代码语言:javascript代码运行次数:0运行复制int x = 1;
int * a = &x;我们现在这里有存储着变量地址的指针变量,那该如何在计算机中找到这个变量呢?
通过解引用操作符‘ * ’就能找的该变量了。如*a;
虽然名字有些绕口,但“解引用”这个名字前缀还是能读出来一些东西。
综上,* 加上指针变量名就等于这个变量,拿上面的代码举例,也就是说这里的*a与x完全等价。
小试牛刀
经过上面对指针的理解,让我们来看看下面这个小练习。
观察下方代码,此时的x打印出的会是1还是100呢?读者看到这不妨自己动手试一试。
代码语言:javascript代码运行次数:0运行复制#include
int main()
{
int x = 1;
int* a = &x;
*a = 100;
printf("%d", x);
return 0;
}答案是:100
3.指针变量的大小我们知道int型变量大小是4字节,char型是1字节,double型是8字节,那么指针变量呢,也和前面的类型有关吗?
代码语言:javascript代码运行次数:0运行复制int main()
{
int x = 1;
int* a = &x;
printf("x的大小是:%d,a的大小是:%d\n", sizeof(x), sizeof(a));
char y = 'y';
char* b = &y;
printf("y的大小是:%d,b的大小是:%d\n", sizeof(y), sizeof(b));
double z = 3.1415926;
double* c = &z;
printf("z的大小是:%d,c的大小是:%d\n", sizeof(z), sizeof(c));
return 0;
}读者不妨复制上述代码在自己编译器上试试。
笔者这里先说结果:这和编译时计算机选择的方式有关,若选择x32(X86),则打印出来就都是4字节;若选x64,则打印出来就是8字节。
那么读者你的计算机打印出来是4字节还是8字节呢?这个4和8又分别是怎么来的呢?
我们知道 1字节=8比特,试着换算一下:32/8=4,64/8=8,正好相等!
这里的32和64,指的是CPU一次能处理的数据宽度,32位CPU一次处理32位数据,也就是4字节,64位则是8字节。
32位系统用4字节指针 → 能管理4GB内存(足够旧电脑使用)。
64位系统用8字节指针 → 能管理海量内存(满足现代需求)。
综上,指针的大小直接由计算机的“位数”决定,而不是指针变量的类型决定。
32位下指针变量大小是4字节,64位下指针变量大小是8字节——目的是为了存储足够长的地址,从而支持更大的内存空间。
注意指针变量的⼤⼩和前边的类型是⽆关的,在相同的平台下,指针变量⼤⼩都是相同的。
4.指针变量的类型既然指针的大小与类型无关,为什么还要有各种各样的指针类型呢?
结论:指针的类型决定了,对指针解引⽤的时候⼀次能访问多少字节。
比如:char *类型指针一次能从内存中访问一个字节,而int * 类型指针一次能访问4个字节。
通常来说,定义的什么类型的变量,相应的指向该变量的指针也是该类型的。
我们知道指针的类型,决定了它一次能读取的字节,所以什么类型的变量用什么类型的指针指向非常关键。比如一个int型变量,如果用char *指针指向,则造成读取的数据错误——原本用四个字节表示的内容,只读了一个字节当然错误了。
特殊指针类型void*
在指针类型中有⼀种特殊的类型是 void * ,可以理解为⽆具体类型的指针。void即空、无效的意思。
void类型的指针可以⽤来接受任意类型地址,相应的, void*指针不可用于+-整数和解引⽤的运算。
那么 void* 类型的指针到底有什么⽤呢?
⼀般void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,以实现泛型编程的效果。(下文将会提到)
5.指针运算核心前提:指针的值是内存地址。 所有指针运算都是围绕内存地址进行的。
1)指针+-整数先观察一段代码
代码语言:javascript代码运行次数:0运行复制#include
int main()
{
int a = 10;
char s = 's';
int* x = &a;
char* y = &s;
printf("加前,x与y的存储的地址:x=%p,y=%p\n", x, y);
x = x + 1;
y = y + 1;
printf("加后,x与y的存储的地址:x=%p,y=%p\n", x, y);
return 0;
}我们分别用x指针变量指向一个int类型变量,用y指针变量指向一个char类型变量。
分别打印出,x与y 加一之前存储的地址,和加一之后存储的地址。
结果是:
由上述实验现象可得:char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。指针+1,其实跳过1个指针指向的元素。当然指针能+1,同理也能-1。
总结:
指针+- 整数的本质含义是:让指针指向的内存地址向前或向后移动 n 个元素大小的位置。
new_add = old_add ± (n * sizeof(指针类型))
关键在于移动的字节数,取决于指针所指向的数据类型的大小 ,编译器会自动根据类型计算正确的字节偏移量。
如,int* x是int类型,一次跳过4个字节,那么x+2,相当于从起始位置向右跳过2*4个字节。
使用场景
常应用于遍历数组,如通过数组名+下标访问数组元素。
深入理解遍历数组
我们知道数组名就是数组的起始地址,数组括号中的数字就是它相对于起始地址加了多少。
比如:有数组int a[3] = {0,1,2};
我们拿a[1]举例,a[1]是在数组的起始地址之上+1:“0 1 2”,0是起始位置,+1后就到1。
所以,数组下标就是相对于数组起始位置的偏移量。
2)指针-指针观察如下代码
代码语言:javascript代码运行次数:0运行复制int my_strlen(char*s)
{
char* p = s;
while (*p != '\0')
{
p++;
}
return p-s;
}
int main()
{
printf("%d", my_strlen("abcde"));
return 0;
}上述代码就是一个简易版的strlen实现代码。
我们已经知道了,指针+-整数,即相较于起始位置跳过n个字节。
指针+-指针含义本质是: 计算两个指针之间相隔多少个元素(不是直接的字节数!)。要求两个指针必须指向同一个连续的内存块(通常是同一个数组,如上述指向同一个字符串)。
指针+-指针的结果 = (add1 - add2) / sizeof(type指针类型)
根据结果的正负可以判断哪个指针在前,哪个在后。
注意:指针-指针可以判断这两个指针之间间隔多少元素(或者数组两个元素之间的距离),而指针+指针则无意义。
使用场景
计算数组中两个元素之间的距离。
3)指针的关系运算关系运算,即两个指针通过>, >=, <, <=, ==, != 等关系运算符进行比较。
核心含义: 比较两个指针所指向的内存地址的高低。
①== 和!= ,比较两个指针指向的地址是否完全相同(是不是同一个变量或者对象)。
②>, >=, <, <=: 检查一个指针指向的内存地址是否高于或低于另一个指针指向的内存地址。
注意:>, >=, <, <= 针指只有两个指针指向同一块连续的内存块时(如数组)才有意义。
使用场景
①如通过“== 和!=”判断指针是否为空NULL;
②>, < 等: 在遍历数组时判断是否到达数组边界(如 ptr < arr + len)。
6.const修饰指针首先const修饰指针变量位置的不同,效果也不同。const,译为常量——变量的对立面。
const 修饰的变量具有常属性,即该变量的值不能被修改。
1)const在*左边如
代码语言:javascript代码运行次数:0运行复制int a = 10;
int const *x =&a;
//或者
const int* x =&a;注意观察上述示例代码。
const如果放在*的左边,修饰的是指针指向的内容——也就是const 修饰的是(*x)。此时(*x)不能改变,但,指针变量x可以改变!
即:*x = 20;❌ ——试图更改(*x)的内容,但(*x)被const修饰了,不能更改。
x = &b;✔ ——没有更改整体的(*x),但更改了x的指向。
上面的现象总结成一句话:const在*左边,指针变量本身内容可变,但指针变量指向的内容不能改变。
2)const在*右边如
代码语言:javascript代码运行次数:0运行复制 int a = 10;
int* const x = &a;const如果放在*的右边,修饰的是指针变量本⾝——也就是const修饰的是x,此时x指针变量的内容不能变。但,是指针指向的内容可以通过指针改变!
即:*x = 20;✔ ——更改(*x)的内容
x = &b;❌ ——试图更改整体的(*x),但x被const修饰了,不能更改。
上面的现象总结成一句话:const在*右边,指针变量本身内容不可变,但指针变量指向的内容能改变。
3)*左右都有const修饰如
代码语言:javascript代码运行次数:0运行复制 int a = 10;
const int* const x = &a;那这下,这个变量就彻底没得玩了:不管是指针变量本身,还是指针变量指向的内容通通不能变。
即:*x = 20;❌
x = &b;❌
7.什么是野指针,如何规避概念:野指针就是指针指向的位置是未知的,不被程序员清楚掌握的。由于不知道野指针指向的是什么内容,如果贸然解引用野指针改变其中的内容可能会引发一些奇怪的bug,甚至程序崩溃。
比如:未初始化的指针
代码语言:javascript代码运行次数:0运行复制int * a;//未出化,a的值随机
*a=20;比如:指针越界访问
代码语言:javascript代码运行次数:0运行复制int a[10];
a[11]=100;//数组只有10个元素,这里越界访问(数组名本质上就是数组的起始地址,a[11]可以理解为*(a+11));
比如:指针指向已被释放的值
代码语言:javascript代码运行次数:0运行复制 int* _test()
{
int tmp = 3;
return &tmp;
}
int* x = _test(); 如何规避野指针
①创建指针变量必初始化;
②时刻关注数组访问,警惕越界;
③指针不用时及时置空(=NULL);
④使用指针之前,检查指针是否为空;
⑤避免返回局部变量的地址。
总之一句话,使用指针前应该尽量做到心中有数。
8.指针经典swap问题:传值调用与传址调用学习是为了解决问题,那什么问题,⾮指针不可呢?
现在我们来尝试写一个交换整形变量值的swap函数,下面的代码正确吗,如果不正确哪错了?
代码语言:javascript代码运行次数:0运行复制void swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 3, b = 4;
swap(a, b);
printf("a=%d,b=%d\n", a, b);
return 0;
}预期结果应该是a与b的值对调,即a=4,b=3,执行结果是?
哪出了问题?
传值调用
在C语言中,当我们将变量传递给函数时发生了“隐形拷贝”。
简单来说,在函数中传入的值系统暗中通过创建一份临时的变量来接收,这个临时变量的值等于我们传入的值,但根本上这个临时变量与我们传入的变量是两个截然不同的变量。这一点可以通过编译器的调试查看变量的地址发现,如
这是传入的变量在内存中的地址,以及a和b的值
这是函数中临时变量的地址,以及a和b的值
通过观察可以看到,尽管他们的值一样,但在内存中的地址不同——完全就是不同的变量。
而临时变量之所以称为临时变量,就是因为他们在函数调用完后,这块内存就会被系统回收,因此上述代码核心逻辑没错,但结果却错的原因就是忽略了系统默认的传值调用。
这种系统默认的调用方式,即传值调⽤。
传址调用
既然找到了问题所在,如何解决呢?
此时指针相关的知识就派上了用场。
我们将欲交换的两个变量的地址传给函数,不管你怎么“隐形拷贝”,传入的值即地址是不会变的。正所谓“跑的了和尚,跑不了庙”,我们在函数中解引用传入的两个地址,直接交换他们本体即可。
综上,正确的swap函数应该是
代码语言:javascript代码运行次数:0运行复制void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int a = 3, b = 4;
swap(&a, &b);
printf("a=%d,b=%d\n", a, b);
return 0;
}
这⾥调⽤Swap函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤
9.数组名的理解我们常说:数组名就是数组首元素的地址。这句话对吗?他对,但不完全对。
既然数组名是首元素地址,那换句话说,数组名应该和指针的大小相同,应该是4或者8才是吧,但下列代码的结果是?
代码语言:javascript代码运行次数:0运行复制 int a[5] = { 5,4,3,2,1 };
int size = sizeof(a);//只传了数组名
printf("%d", size);结果
很显然,这里的20是整个数组的的大小,而非首元素的大小。不是说数组名就是数组首元素的地址吗?
两个例外‘'数组名就是数组首元素的地址"这句话本身是没毛病的,但有两个例外:
①就是上述的sizeof(数组名),这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩;
②&数组名:这里取出的是整个数组的地址,而不是取出的数组⾸元素的地址,这是有区别的。
“有区别的”,这个区别在哪呢?观察下列代码
代码语言:javascript代码运行次数:0运行复制 int a[5] = { 5,4,3,2,1 };
int size = sizeof(a);
printf("&a=%p\n", a);
printf("&a[0]=%p\n\n", &a[0]);
printf("(&a)+1=%p\n", (&a)+1);
printf("(&a[0])+1=%p\n", (&a[0])+1);结果
我们看到直接打印a和a[0]的地址是一样的,则可以理解,毕竟“数组名就是数组首元素的地址”。但将他们的地址+1后就看出不同了:
&a和(&a)+1相差20个字节(这里用的16进制),这就是因为&a是数组的地址,+1操作是跳过整个数组的。而相应的(&a[0])+1相差不过4个字节,刚好一个int变量大小。
这就是“&数组名和和&数组⾸元素的地址是有差别”的差别所在。
正是由于上述的两个例外,在遍历数组时额外方便
如
代码语言:javascript代码运行次数:0运行复制 int a[5] = { 5,4,3,2,1 };
int size = sizeof(a)/sizeof(a[0]);
for (int i = 0; i < size; ++i)
{
printf("%d ", a[i]);
} //用数组的总大小除以单个元素的大小,得到数组的元素个数
//a[i]的本质为*(a+i) ——数组下标本质上是相对于数组首元素的偏移量。
数组传参的本质经过上述得知,数组名是数组⾸元素的地址。那么在数组传参的时候,传递的是数组名,也就是说数组传参传递的本质是:数组首元素的地址,而非整个数组的地址。
如果真是这样,那在函数中应该是求不出数组元素的个数的。我们通过下列代码验证:
代码语言:javascript代码运行次数:0运行复制//在函数内部求不出数组大小
void arr_size(int a[])
{
int size = sizeof(a) / sizeof(a[0]);
printf("%d", size);
}
int main()
{
int a[5] = { 5,4,3,2,1 };
arr_size(a);
return 0;
}//之所以形参写成int a[ ],是因为写int a函数误以为这是个变量
结果
果然在函数内部数求不出数组大小。
总结:在函数中数组传参,形参的部分可以写成数组的形式,如int a[ ];也可以写成指针的形式,如int * a。
/ /在函数中传递数组会丢失大小信息:这是C/C++的固有特性,故仍需额外传递大小参数。这体现了 C/C++ 的核心设计哲学:效率优先、明确控制、沿袭设计。
10.二级指针经过上面的学习,我们知道指针变量本质上也就是个变量,只不过它里面存储的是他所指向的变量在内存中的地址。
那既然指针变量也是个变量,那他在内存中岂不是也有地址?
二级指针:用于存储指针变量在内存中的地址。
如果按照上述理论的话,我们将二级指针两次解引用应该会得到“指针的指针所指向的变量”。
即int a =10;int *p=&a;int **pp=&p;
那*(*p)应该等于a,即10;
下面的代码将验证这个猜想
代码语言:javascript代码运行次数:0运行复制 int a = 10;
int* p = &a;
int** pp = &p;
printf("**pp=%d\n", **pp);
printf("a的地址:%p\n", &a);
printf("一级指针p的地址:%p\n", &p);
printf("二级指针pp的地址:%p\n", &pp);结果
**pp的值==a ==10符合上述推论,下图详解二级指针**pp解引用过程:
①pp指针变量中储存着p的地址,通过*pp我们拿到了p的地址,即*pp == p
②p指针变量中储存着a的地址,通过*p可以访问a,即*p==a;
③**pp 可以理解为*(*pp),首先(*pp)的值等于p,那么*(*pp)的值就等于a了,即*(*pp)=*p=a。
有二级指针,当然就还有三级指针 四级指针……,但原理都是一样的。理解了二级指针,不管他怎么“套娃”我们理解起来都游刃有余。
基础指针知识小结 1)指针变量,归根到底是一个变量,只不过这个变量中存储的是其他变量的地址。
2)指针变量的大小由计算机决定:是32位计算机,则指针大小为4字节;是64位计算机,则指针大小为8字节。
3)不同类型的数据在内存中占用的字节数不同,如char 型占用1字节,int 型占用4字节。而不同类型的指针,本质上是能访问不同大小的内存块,如char*型指针能访问的内存块大小为1字节,int*类型的指针能访问的内存块大小为4字节。
这三条是指针基础的基础,其他指针相关的知识都是以这三条为基础发散的,如cost修饰后的指针、指针的运算等等。所以上面的三条尤为重要,在这里我们再一次理解并记忆他。
接下来,让我们进入指针学习的第二阶段——指针进阶。
——书山有路勤为径
二、指针进阶在有了一定的指针基础之后,我们来尝试理解一些更常用更重要同时也更容易混淆的指针及相关知识。
1.字符指针的两种用法字符指针char*有两种用法。
1)储存单个字符
如
代码语言:javascript代码运行次数:0运行复制 char ch = 'c';
char *pc = &ch;
*pc = 'h';我们可以通过char*类型的指针访问或者更改一个char类型的变量。
2)储存字符串的首元素地址
如
代码语言:javascript代码运行次数:0运行复制const char* str = "hello world";我们知道字符串本质上就是带有常属性的字符数组。
换句话说这里的"hello world",可以理解为const char s[12] = { 'h','e','l','l','0',' ','w','o','r','l','d','\0'};
于是就有const char *str = const char s[12];
还记得数组名等于数组第一个元素的地址吗,这里也是一样的,str指针变量本质上储存的是数组第一个元素‘h’的地址,而非整个数组的地址。
注意细节:
①为什么这里的数组元素个数是12呢?
在C/C++中,字符串被定义为以\0结尾的字符序列,所以字符串的末尾默认带了一个'\0',但C/C++默认隐藏了他。
②为什么要用const修饰这个数组?
由于字符串被定义后不能被修改,所以只能用const修饰的指针来指向字符串。
2.指针数组与数组指针指针数组与数组指针是两个极易混淆的概念,下面我们先介绍什么是“指针数组”,什么是“数组指针”,再介绍他们的区别。
1)指针数组什么是指针数组,指针数组是指针还是数组?
类⽐⼀下,整型数组,是存放整型的数组,字符数组是存放字符的数组。同理可得指针数组,是存放指针的数组。
指针数组的每个元素是地址,⼜可以指向内存中的另⼀块区域。
2)数组指针数组指针,顾名思义他就是一个指针。指针变量用来存储普通变量的地址,数组指针用来存储数组的地址。
数组指针本质上就是一个指针,这个指针指向一个完整的数组,而不是指向数组的第一个元素。
指针数组常见的声明定义方式为:
数据类型 (*指针名)[数组大小]。
比如
代码语言:javascript代码运行次数:0运行复制int (*arr_p)[5]=&arr;其中arr_p是指针名,int (*)[5]是指针类型。上述代码的表示:一个类型为int (*)[5],变量名叫arr_p的数组指针指向arr的地址。
注意:
①数组名一般代表数组的首元素的地址,想取整个数组的地址应该在数组名前加&,如&arr;
②数组指针的类型,需要与被指向数组的类型相同,括号中的数字需要与被指向数组的大小相等。
3)指针数组与数组指针的区别经过上述分析,指针数组和数组指针在 C/C++ 中是两个完全不同的概念!
区别1)两者本质不同
指针数组本质是数组,其中数组元素是指针;
数组指针本质是指针,用于指向数组的地址。
区别2)两者优先级不同
例如:
指针数组:int *p[5];其中 [] 优先级高,所以 p 先是数组,数组的元素是指针 (int*)。
//先数组再指针。
数组指针:int (*p)[5]; 其中() 优先级最高,所以p 先是指针,然后 [5] 修饰 *p,表示 p 指向一个数组。数组指针。
//先指针再数组
3.二维数组的传参本质二维数组,即形如int a[3][5],char [4][6]的数组。
二维数组本质上是一个数组的数组,即每个元素本身又是一个数组。在内存中,二维数组的元素通常是连续存储的,先存储第一行的所有元素,然后是第二行,依此类推。
那么问题来了,二维数组如何传参呢?
①实参怎么定义,形参就怎么定义
//实参:指调用函数时传入的参数,如下面的test(arr, 3, 5)中的arr、2、5就是实参
//形参:指函数声明或者实现时,函数名后的括号中定义的变量,如下的int a[2][5], int r, int c都是形参。形参属于临时变量,出函数即马上销毁。
由于数组在传到函数中后,无法计算数组大小。所以在传参时也要把数组大小传进去。
如
代码语言:javascript代码运行次数:0运行复制 #include
void test(int a[2][5], int r, int c)
{
}
}
int main()
{
int arr[2][5] = {{1,2,3,4,5}, {2,3,4,5,6}};
test(arr, 3, 5);
return 0;
}②通过数组指针传参
我们再来看一下二维数组的本质:⼆维数组的每一个元素就是个⼀维数组。
那么在二维数组传参时,由于数组名即第一个元素的地址,而第一个元素又是一个一维数组,于是二维数组传入函数的本质是一个数组指针,这个指针向了首元素的一维数组。
如上述代码中传入的int a[2][5],由于编译器自动将其转换为指向首元素的指针,那么就变成了一个int (*)[5]的数组指针。
既然实际上传入的是一个数组指针,那么我们完全可以在定义形参时就定义成一个数组指针。
如
代码语言:javascript代码运行次数:0运行复制 void test(int (*)[5], int r, int c)
{
}总结:由于数组传参时发生了数组名到指针的退化,所以⼆维数组传参的本质其实是传入了一个数组指针。同理三维、四维数组传参也是如此。
4.函数指针变量什么是函数指针变量?
1)函数指针的概念通过类⽐整型指针、数组指针,我们可以得出函数指针变量是用来存储函数的地址的,并且可以通过函数指针调用函数。
与变量一样,函数是有地址的,函数名就是函数的地址,也可以通过&函数名获得函数的地址。
函数指针变量的写法与数组指针变量非常相似
数据类型 (*函数指针变量名)(函数形参)
比如
代码语言:javascript代码运行次数:0运行复制int (*add)(int a,int b)上述函数指针变量名add,指针类型为int (*)(int ,int );
2)函数指针的使用既然有了指针变量,我们自然是可以通过*解引用他。
使用函数指针的语法为:
(*函数指针变量名)(实参列);
①先通过*与(),将指针变量名解引用;
②再通过()传入实参。
如
代码语言:javascript代码运行次数:0运行复制#include
//函数指针
int my_add(int a, int b)
{
return a + b;
}
int main()
{
int (*add)(int, int) = my_add;
int sum = (*add)(5, 5);
printf("%d\n", sum);
return 0;
}3)函数指针数组既然有了函数指针,那么函数指针数组应该如何创建呢,‘[ ]’符号应该放在哪呢?
在遇到未涉及过的问题时,我们首先应该想到的是能否通过已有的知识推断解决它。
比如,我们知道普通指针数组的创建方式,int *arr[ ];或者char *arr[ ]。 我们不难发现[ ]符号都是放在指针变量名之后的。
同理,函数指针数组的创建也是将[ ]放在指针变量名之后。
如
代码语言:javascript代码运行次数:0运行复制int (*add[3])(int,int);
//函数指针
int my_add(int a, int b)
{
return a + b;
}
int main()
{
int (*add[3])(int, int) = { my_add,my_add,my_add };
for (int i = 0; i < 3; ++i)
printf("%d\n", (*add[i])(i, 10));
return 0;
}
综上,就是本文与指针相关的全部知识了,有兴趣的读者可以阅读下面补充的回调函数,看不懂也没关系哦。
补充:回调函数什么是回调函数?
当我们把函数的指针作为参数传递给另⼀个函数时,若这个指针在函数中被⽤来调⽤其所指向的函数,被调⽤的函数就是回调函数。
简单来说,如果在函数中通过函数指针调用因一个函数,这个被调用的函数就叫回调函数。
如
代码语言:javascript代码运行次数:0运行复制//回调函数
void s()
{
printf("正在执行函数s\n");
}
void s1()
{
printf("我是函数s1\n");
}
void test(void(*s)(),void(*s1)(),int n)
{
int num = n % 2;
if (num == 0)
(*s)();
else if
(num == 1)(*s1)();
else
printf("bug?");
}
int main()
{
test(s, s1, 5);
return 0;
}在上述代码中我们可以通过控制传入test函数值的不同,决定是执行s函数还是s1函数。
回调函数与函数嵌套调用的区别是?
在目前阶段,我们可以认为回调函数 ≈ 嵌套调用。但实际情况更为复杂,回调函数实际上分为同步回调与异步回调,我们现在认识的回调函数就是同步回调函数。而异步回调需要结合操作系统等相关知识理解,如多线程的引入或者是定时器。
对回调函数感谢兴趣的读者可自行查找相关资料学习,笔者补充回调函数的主要目的是想说明“学无止境,持续学习的重要性,以及漫漫学习路上永远怀揣着一颗的”小白之心“的必要性。
总结本篇文章大体分为两个阶段:
第一阶段是介绍指针及其相关的基础知识;第二阶段则介绍一些容易混淆的指针相关知
到目前位置,本文一共11796个字,其中超过半数用于介绍关指针的基础知识,也就是第一阶段。
笔者水平有限,若文中出现错误,还恳请读者在评论区中指出,笔者当第一时间查证更改。