C语言指针这一篇够了(一万二千字,包含指针与数组,函数指针等详解)
1.指针的定义指针(Pointer)就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。指针变量可以存放基本类型数据的地址,也可以存放数组、函数以及其他指针变量的地址。int a=10;//定义一个变量aint* pa=&a;//定义一个指针pa指向a*pa=9;//通过对指针的解引用修改a的值其中修改a的值得过程与 a=9 等价。注意两个‘*’的意义不同,第一个‘*’是
目录
零.前言
相信很多小伙伴学c语言的时候都是谈‘针’色变呀,我当年学的时候也可谓之吃力,因此写了这篇文章,希望帮助更多的在指针的剥削下瑟瑟发抖的同学们彻底攻克这一烦人的内容,让指针的压迫成为过去时,让更多的同学看完这篇文章后可以对着指针大喊:我!站起来了!
一.指针的定义
指针(Pointer)就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。指针变量可以存放基本类型数据的地址,也可以存放数组、函数以及其他指针变量的地址。
int a=10; //定义一个变量a
int* pa=&a;//定义一个指针pa指向a
*pa=9; //通过对指针的解引用修改a的值
其中修改a的值得过程与 a=9 等价。
注意两个‘*’的意义不同,第一个‘*’是与int连用的,表示指针类型是(int*),第二个'*'是解引用操作符。
二.指针类型的意义
在32位机器中,每一个指针的大小都是四个字节,直接给指针一个统一的类型比如ptr*不香吗?那为什么还要有像(int*),(char*)等指针类型的区分呢,下面介绍指针类型的意义。
1.指针类型决定了指针解引用时一次访问几个字节。
int a = 0x11223344;
首先我们定义一个16进制的数字a。
我们先找到a的地址,然后给他赋值,在内存中观察到的是这样的。
此时a的地址时0x0095F8FC,可以看到已经将16进制数字0x11223344放入了a中。
下面我将用不同类型的指针分别修改a的值,我们可以看看内存中a的值得变化效果。
1.int* 定义的指针
int* pa=&a;
*pa=10;
当执行完*pa=10之后,a中放的内容为
可以发现a的四个字节都发生了改变,并且这四个字节组成的数字是10。
2.char*定义的指针
char* pa=&a;
*pa=10;
最后a的值是这样的
我们会发现只改变了一个字节的内容,改动的那个字节被赋值成10,但a的值并不是10。
总结一下就是int*型指针在解引用修改指向值时,修改的是和整型一样大小的4个字节的内容,而char*型指针在解引用修改指向值时修改的是和字符型一样大小的1个字节的内容。
2.指针类型决定了指针加减整数时的步长
int a = 10;
int* pa = &a;
char* pb = &a;
printf("%p\n", pa);
printf("%p\n", pb);
printf("%p\n", pa + 1);
printf("%p\n", pb + 1);
这个代码的输出结果是
我们发现int*型指针+1后的结果多了4个字节,但char*型指针+1后的结果只多了1个字节。
三.野指针
1.未初始化的指针
int *p;
*p=10;//编译器会报错
2.指针的越界访问
int arr[10];
int i;
int*p=arr;//数组名表示数组首元素地址
for(i=0;i<=10;i++)
{
*p=i;
p++
}
由于数组中只有10个元素,但是指针已经访问到第十个元素的下一个元素了,但该元素没在定义的内存中,最终p成为野指针。
3.指针指向空间的释放
int* test()
{
int a=10;
return &a;
}
int main()
{
int*p=test();
printf("%d",*p);
return 0;
}
这样一段代码就发生了指针指向空间的释放,当test()函数开始执行的时候,内存为它开辟空间,当执行结束之后,这块空间会还给内存,即p接收的是不属于我们已开辟空间的地址。
但是打印*p得到的依然是10,这是由于虽然之前为a开辟的空间已经还给了内存,但是空间中的内容是不变的,p接收了这段不在内存空间的地址,*p的值是10。
int* test()
{
int a=10;
return &a;
}
int main()
{
int*p=test();
printf("hehe");
printf("%d",*p);
return 0;
}
但是如果我们在打印*p之前随意引用一个函数,比如printf(),那么*p的结果就不一定会是10,这是因为,每新调用一个函数,内存都要为它分配空间,由于test()的空间已经释放,所以为printf()分配的空间就有可能会覆盖到原来为test()分配的空间上,从而导致对原来存储a的空间内容的修改,使*p的值不再是10。
四.指针运算
1.指针加减整数
这个在上面指针类型的意义中讲过,这里不多赘述。
2.指针减去指针
指针减指针有一个前提就是要在同一块空间内进行,例如在同一个数组内
int arr[10]={1,2,3,4,5,6,7,8,9,0};
printf("%d",&arr[9]-&arr[0]};//9
指针减指针的结果是这两个指针之间间隔的元素个数。打印的结果是9。
3.指针比较大小
指针比较大小即为地址比较大小。
int arr[10]={1,2,3,4,5,6,7,8,9,0};
int* p;
for(p=arr[10];p>&arr[0];)
{
*--p=0;
}//由于是后置--,并没有对arr[10]进行操作,所以可以这样使用
其中p>&arr[0]为对指针的关系运算。
五.指针与数组
数组:一块连续的空间,存放的是相同类型的元素,数组的大小与元素类型和个数有关。
指针:是一个变量,存放地址,大小是4(32位机器)或者8个(64位机器)byte。
1.数组名
数组名大多数情况表示的就是首元素地址。
int arr[10]={0};
printf("%p",arr);
printf("%p",&arr[0]);
这两段代码打印的结果是相同的,说明数组名表示的是首元素地址。
但是有两个特例
(1)当与sizeof()结合的时候
int arr[10]={0};
int sz=sizeof(arr)/sizeof(arr[0]);//10
这段代码打印的结果是10,当sizeof与数组名结合的时候数组名表示整个数组。sizeof(数组名)表示的是整个数组所占的字节数。
(2)当&数组名时
int arr[10]={0};
printf("%p\n",arr);
printf("%p\n",&arr);
printf("%p\n",arr+1);
printf("%p\n",&arr+1);
打印出来的结果是这样的,打印出来的arr和&arr是一样的,但是+1之后,arr加了4个字节,但是&arr加了40个字节,说明 &arr中的arr表示的是整个数组。
2.用指针操作数组
这里只是另一种操作数组的方式。
int arr[10]={0};
int* p=arr;
int i;
int sz=sizeof(arr)/sizeof(arr[0]);
for(i=0;i<sz;i++)
{
*(p+i)=i;
}
其中*(p+i)与arr[i]是等价的。
3.指针数组
指针数组,顾名思义就是存放指针的数组,我们可以类比:
int arr[10];//整型数组,存放整型的数组
char ch[5];//字符数组,存放字符的数组
int* parr[10];//指针数组,存放指针的数组
即数组中存放的全都是指针。举个例子:
int a=10;
int b=90;
int c=40;
int* parr[3]={&a,&b,&c};
将a,b,c的地址存放在了指针数组中。
再来看一段代码:
#include<stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* arr4[] = {arr1,arr2,arr3};
int i,j;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", arr4[i][j]);
}
printf("\n");
}
return 0;
}
我们可以看到这段代码的运行结果。注意printf后面的部分用法,arr4[i][j]并不是二维数组,假设我们要得到arr1中1这个元素,首先通过arr4[0]找到arr1的首地址,然后加0得到1的地址,再对其解引用,这一过程可以写成*(arr4[0]+0),类比一维数组中*(arr+i)与arr[i]等价,则*(arr4[0]+0)与arr4[0][0]等价。所以才有了arr[i][j]这一用法。
4.数组指针
(1)数组指针与指针数组的区别
数组指针与指针数组不同,前者强调的是指针,后者强调的是数组。
首先我们还是先进行一下类比:
int* p;//整型指针,即指向整型的指针
char* pc;//字符指针,即指向字符的指针
int(*p)[10];//数组指针,指向数组的指针
注意数组指针与指针数组的区分
int* p[10]表示的是p[10]是一个存放指针的指针数组,其中p是数组名。
int(*p)[10]表示的是p是一个指向有十个元素的数组的指针,且这十个元素都是整型。
在这里数组指针的类型是int(*)[10]。
(2)数组指针的使用
1.一维数组传参
来看这样一段代码
#include<stdio.h>
void print1(int* p, int sz)
{
int i;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
void print2(int(*p)[10], int sz)
{
int i;
for (i = 0; i < 10; i++)
{
printf("%d ", *(*p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
print1(arr, sz);
printf("\n");
print2(&arr, sz);
return 0;
}
这段代码用两种不同的传参方式,打印出来相同的结果。
向print1向传的参数是数组首元素的地址。
向print2这个函数传的参数是整个数组的地址,因为&数组名表示的是真个数组的地址。
print1接收的是首元素地址,只需要一个整型指针就可以接收。
print2接收的是整个数组的地址,所以需要用指针数组来接收。
重点讲一下print2函数是如何将数组的所有元素成功打印的:
重点在于这个部分,首先我们知道p是一个指向整个数组的指针。
规定:对指向整个数组的指针进行解引用的结果是整个数组的首元素的地址。
则*p是数组arr的首元素的地址,结合之前讲的用指针操作数组,可以知道*(*p+i)表示的是数组中的元素,其中*p可以改写成*(p+0),所以p[0]表示的也是数组首元素的地址,整个表达式可以改写成这样:p[0][i],这与上面的表达式是等价的。
对于一位数组,其实并不建议这么传参,原因是比较绕,容易混淆。
2.二维数组传参
来看这样一段代码
#include<stdio.h>
void print1(int arr[3][5], int r, int c)
{
int i, j;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print2(int(*p)[5], int r, int c)
{
int i, j;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };
print1(arr, 3, 5);
printf("\n\n");
print2(arr, 3, 5);
return 0;
}
我们可以看到,接收参数的方式不同,打印的结果是相同的,其中print1是常规操作,我们重点说一下print2这个函数。
首先我们要知道:二维数组的数组名表示的是第一行的地址。
这也是我们可以用数组指针来接收二维数组数组名的原因。
整个问题的关键所在还是在这段代码上。
我们已经明确p表示的是第一行的地址,那么根据指针运算的部分我们可以知道(p+i)表示的是第i行的地址,对(p+i)解引用,找到的是第i行第一个元素的地址,对这个地址+j表示的是这一行第j个元素的地址,再解引用,表示的就是第i行第j列的元素内容。
由于*(p+i)等价于p[i]所以整个表达式等价于p[i][j]。打印的结果是一样的。
5.总结
1.区分概念
int arr[5];//一个整型数组
int *parr[10];//一个指针数组,数组的每个元素都是指针
int(*parr2)[10];//一个数组指针,这是一个指向含有十个整型元素的数组的指针
int (*parr3[10])[5];//parr3是一个指针数组,数组中存放的是指针
//parr3中每一个元素指向含有5个整型元素的数组
2.一维数组传参
int arr[10]={0};
test(arr);
现在定义一个数组,以数组名为参数,形参的几种形式
void test(int arr[]);
void test(int arr[10]);
void test(int *arr);//数组名是首元素地址可以用指针接收
若定义的值一个指针数组:
int *arr[10]={0};
test(arr);
形参的形式可以是:
void test(int *arr[10]);
void test(int **arr);//由于数组名时首元素地址,首元素是一个指针,能够接收指针地址的是二级指针
3.二级指针传参
int arr[3][5]={0};
test(arr);
形参的形式可以是:
void test(int arr[3][5]);
void test(int arr[][5]);//注意列不可以省
void test(int (*p)[5]);//二维数组数组名表示的是第一行的地址
六.二级指针
我们知道指针存放的是某一个元素的地址,但是指针依然是一个变量,指针也有它自己的地址。
int a=10;
int*p=&a;
printf("%d",*p);//10
int**pp=&p;//定义二级指针pp,并使pp指向p的地址
printf("%d",**p);//相当于两次解引用,结果依然是10
那你可能会问了二级指针的地址是不是用int***类型的变量存储呢,答案是肯定的,理论上来说是可以无限套娃的,但是用多了之后你会发现代码会变得冗余可读性变差,所以不建议无限套娃。
七.字符指针
1.第一种形式
char ch='w';
char*p =&ch;
这种形式和整型指针一样,指针指向一个字符的地址。
2.第二种形式
char*pc="hello world";//hello world是一个常量字符串,存放在内存的常量区,不允许改
printf("%c",*p);//打印出来的是第一个字符h
printf("%s",p);//打印出来的是整个字符串hello world
当指针指向字符串时,对它解引用得到的是字符串的第一个字符,当打印整个字符串时,将%c改成%s,不需要再解引用,这是由printf这个函数本身性质所决定的。
根据这一特征我们可以通过指针数组打印多组字符串。
int i;
char* arr[3]={"abcd","efgh","ijkl"};
for(i=0;i<3;i++)
{
printf("%s",arr[i]);
};
值得注意的是,此时的hello world是一个常量字符串,是无法通过*p修改的,如果加上*p='w'的话,编译器会报错。
八.函数指针
所谓函数指针,就是可以指向函数的指针,函数和各类型变量一样,也有自己的地址。
1.函数的地址
#include<stdio.h>
int add(int a, int b);
int add(int a, int b)
{
return a + b;
}
int main()
{
printf("%p\n", add);
printf("%p", &add);
}
这段代码打印的结果是
我们可以看到,无论是否在函数add前加上&,打印出来的结果是一样的,这说明add和&add都代表着函数的地址。
2.函数指针
既然函数有地址,那么一定有相应类型的指针变量去接收它的地址。并有相应的解引用操作。我们用函数操作和函数指针操作进行一下对比。
#include<stdio.h>
int add(int a, int b);
int add(int a, int b)
{
return a + b;
}
int main()
{
int (*p)(int, int) = add;
int ret = add(2, 3);
printf("%d\n", ret);
ret = (*p)(2, 3);
printf("%d", ret);
return 0;
}
1.函数指针的定义
这一部分为函数指针的定义部分,前面讲过,add可以表示函数的地址。这里的函数指针名为p,p前面的*代表它是一个指针,后面的(int,int)表示这个指针所指向函数的参数为两个int型的参数,最前面的int表示的是这个指针所指函数的返回值是int型。该函数指针的类型为int(*)(int,int)。
2.函数指针的解引用
我们可以看到,这段代码打印的结果依然是5,这里的p是一个指向函数add的指针,*对p解引用得到函数add,再向add传参2和3,从而得到add的返回值5。
规定:在解引用函数指针时,*可以填多个,也可以不填。也就是说ret=(*p)(2,3)可以改写成p(2,3),也可以改写成(**p)(2,3)。
3.函数指针的两个复杂的例子
(*(void(*)())0)();
我们来分析一下这段代码,分析这种复杂的代码时一定要先找到每个括号的位置。
我们将它一层一层展开,首先是这一部分(void(*)())0,前面括号里面的void(*)()是一个没有返回值,没有参数的函数指针类型,后面的0是一个整型,在函数指针类型上加括号表示的是,将0这个整型强行转换成函数指针类型。即0变成了一个函数指针。
现在的代码被简化成了(*0)(),其中0是函数指针类型,对其解引用找到了这个函数,由于0这个函数指针的类型是void(*)(),所以对0这个函数指针解引用时要传递的类型就是(),即原式是对0这个指针变量解引用的结果。
void(*signal(int,void(*)(int)))(int);
再来看这样一段代码,signal(int,void(*)(int))这一段先不看先用a来代替,剩余的就是void(*a)(int)这样一段内容,很明显这是一个函数指针的定义,即a就是一个函数指针,即signal(int,void(*)(int))是一个函数指针,即signal是一个函数,它的返回值是一个函数指针,它的参数类型是整型int和函数指针类型void(*)(int)。
这样一段代码看起来很复杂,可以用typedef来简化一下,我们可以先这样定义:
typedef void(*p)(int) ,注意这不是定义了一个函数指针,而是将函数指针类型定义成了p,即p代表void(*p)(int)这一类型,这样的话原代码就可以改写成p signal(int,p),比原来看起来会简便多了。
3.函数指针的应用
函数指针可以简化很多操作,尤其是对那种需要调用多个相似函数的程序有奇效。原理就是可以将每一个相同类型的函数放在函数指针数组中。
我们拿实现计算器作一个对比
首先是没用到函数指针的计算器:
#include<stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
void menu()
{
printf("**1.add***\n");
printf("**2.sub***\n");
printf("**3.mul***\n");
printf("**4.piv***\n");
printf("**0.exit**\n");
}
int main()
{
int input=0;
menu();
do {
printf("please input your choice!\n");
scanf("%d", &input);
int a, b,ret;
switch (input)
{
case 1:scanf("%d %d", &a, &b);
ret=add(a, b);
printf("%d\n", ret);
break;
case 2:scanf("%d %d", &a, &b);
ret=sub(a, b);
printf("%d\n", ret);
break;
case 3:scanf("%d %d", &a, &b);
ret = mul(a, b);
printf("%d\n", ret);
break;
case 4:scanf("%d %d", &a, &b);
ret = div(a, b);
printf("%d\n", ret);
break;
case 0:printf("exit!");
break;
default:printf("error!\n");
break;
}
} while (input);
return 0;
}
这个程序很容易理解,用switch语句选择不同的计算操作就能实现计算功能。然而这段代码还是国语冗余,我们可以思考一下函数指针的方法。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
void menu()
{
printf("**1.add***\n");
printf("**2.sub***\n");
printf("**3.mul***\n");
printf("**4.piv***\n");
printf("**0.exit**\n");
}
int main()
{
int input = 0;
menu();
int (*p[5])(int, int) = { 0,add,sub,mul,div };//定义一个函数指针数组用来存储四个函数的地址,0起到补位作用。
do {
scanf("%d", &input);
if (input == 0)
{
printf("exit\n");
break;
}
else if (input >= 1 && input <= 4)
{
int a, b, ret;
scanf("%d %d", &a, &b);
ret = (*p[input])(a, b);//对函数指针进行解引用找到对应的函数。
printf("%d\n", ret);
}
else
{
printf("error!\n");
}
} while (input);
return 0;
}
这样一来的话判断语句更简便了,所需要的代码量也变少了。得到的结果是相同的。
3.回调函数
1.定义
定义:回调函数就是一个通过函数指针调用的函数,如果你把函数的地址作为参数传给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
仍然拿计算器这个程序举例:
#include<stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
void calc(int (*p)(int, int))//定义一个函数接收各函数地址
{
int a, b, ret;
scanf("%d %d", &a, &b);
ret = (*p)(a, b);
printf("%d\n", ret);
}
void menu()
{
printf("**1.add***\n");
printf("**2.sub***\n");
printf("**3.mul***\n");
printf("**4.piv***\n");
printf("**0.exit**\n");
}
int main()
{
int input = 0;
menu();
do {
printf("please input your choice!\n");
scanf("%d", &input);
int a, b, ret;
switch (input)
{
case 1:calc(add);
break;
case 2:calc(sub);
break;
case 3:calc(mul);
break;
case 4:calc(div);
break;
case 0:printf("exit!");
break;
default:printf("error!\n");
break;
}
} while (input);
return 0;
}
这个方式也比第一种方式简单的多,新定义了一个函数接收各个函数的地址,在每个case中只需要引用一个calc函数就可以了,这种方式利用函数指针解决了每个case中相同代码较多的问题。
回调函数还是很重要的,刚刚看了几个指针的面试题,都有涉及到回调函数,所以再给大家举一个例子。
2.qsort函数(void*)
我们知道c语言中有许多我们还很少用到的库函数,比如qsort函数,这是一个快速排序函数,和回调函数就有很大的联系。这个函数的功能很强大,不仅能排序整数,甚至结构体便量也可以排序。这里只排整型变量,对排结构体感兴趣的小伙伴可以研究一下呀。
首先我们先看一下这个函数的参数:
这是msdn中给到的函数定义,我们逐个来介绍
1.void*base:定义了一个空类型的指针,空类型的指针可以指向所有类型的地址,但是不能解引用和进行指针加减运算。这里的base表示的是指向首元素的指针。
2.size_t num和size_t width:前者代表的是元素的个数,后者代表每个元素所占字节数。
3.int (__cdecl*compare)(const void*elem1,const void*elem2):这里定义了一个函数指针,所指向的是判断相邻两个数据的大小的一个函数即传了一个函数名,也是函数的地址。对这个函数也有一定的要求。这里的两个数据也是先用的void*来接收的地址。
下面用一段代码来展示一下这个函数怎么用:
#include<stdio.h>
#include<stdlib.h>
int compare(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), compare);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
注意我们向qsort函数传的参数
compare函数的参数是两个空类型的指针,传入的是arr数组两个相邻数的地址,前面讲过,void*类型是不能解引用的,所以我们要先将其强制转换成int*类型,然后再解引用。
英语差的我还特意百度了一下这段话的意思,就是说当e1>e2时,如果我定义的函数返回值大于0的话,那么e1就会排在e2的后面,反过来如果我定义的函数返回值小于0,那么e1就会排在e2的前面。
4.指向函数数组指针的指针
我们还是逐层取理解
int (*p)(int ,int)=add;//p是一个函数指针
int (*p[4])(int,int)={add,sub,mul,div};//p是一个函数指针数组,每个元素存放一个函数地址
int (*(*p)[4])(int, int);//p是一个指向函数指针数组的指针
这段代码如何理解呢?首先看(*p)[4]这一部分,*与p结合表示p是一个指针,再与[10]结合,表示这个指针指向的是一个数组,该数组里有4个元素,在将(*p)[4]排除,剩余部分为int (*)(int,int),这一部分代表数组中每一个元素的类型,类型是函数指针类型,所以p是一个指向函数指针数组的指针。
九.总结
感谢大家可以看到这里,呕心沥血了接近5天,总算是呕完了,喜欢的别忘收藏一下,关注一下我啥的,不知道小伙伴们站起来了吗,全文12000字,又破记录了,以后还想写一个指针的面试题的总结,加深一下这一块内容的理解,学习指针调试的方面也很重要但这里没多赘述,最后祝大家学业有成吧(比心)。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)