✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
个人主页:@rivencode的个人主页
系列专栏:玩转C语言
推荐一款模拟面试、刷题神器,从基础到大厂面试题点击跳转刷题网站进行注册学习

目录

  • 前言
  • 一. 什么是指针
    • 1.地址如何产生
    • 2.指针变量和指针和地址关系
    • 3.指针的大小和类型
    • 4.指针的运算
    • 5.二维指针
    • 6.字符指针
    • 7.指针的类型,与指向元素的类型
  • 二.野指针
    • 1.野指针怎么来
    • 2.如何规避野指针
  • 三.指针与一维数组(要理解后面这里很重要)
    • 1.数组名
    • 2.指针与数组深度理解
    • 3.数组的类型
  • 四.指针数组
    • 1.指针数组的概念
    • 2.指针数组的应用
  • 五.数组指针
    • 1.数组指针的概念
    • 2.指向指针数组的指针
    • 3.数组指针与二维数组
  • 六.数组与指针传参
    • 1.一维数组传参
    • 2.二维数组传参
    • 3.一级指针传参
    • 4.二级指针传参
    • 5.总结
  • 七.函数指针
    • 1.函数的地址
    • 2.定义一个函数指针
    • 3.函数指针数组
    • 4.用函数指针数组实现简易计算器
    • 5.指向函数指针数组的指针
  • 八.回调函数
    • 用回调函数实现简易计算器
  • 九.汇总

前言

指针就是C语言的灵魂,想要学好C语言指针这一关必须过,既然是灵魂必须难度必然不小,但并没有想象中那么难,向学好指针必须要学会类比,深入理解你会发现都是按照套路来的,学指针前字符串,一维数组,二维数组,函数,指针的类型,数组的类型,函数的类型等基本概念弄清。

一. 什么是指针

在计算机中,所有的数据都是存放在存储器中的,不同的数据类型占有的内存空间的大小各不相同。内存是以字节为单位的连续编址空间,每一个字节单元对应着一个独一的编号,这个编号被称为内存单元的地址而这个地址就是指针,后续可以通过指针来访问内存单元的内容

1.地址如何产生

内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。所以为了有效的使用内存,就把内存划分成一个个小的内存单元, 每个内存单元的大小是1个字节 。为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。

如何产生地址:
若是32位的操作系统,则有32地址线/数据线,就可以简单理解成电线则没根地址线都可以产生高电平和低电平也就是数字信号 1 和 0,这也是计算机只能存储二进制数的原因。


变量是创建内存中的(在内存中分配空间的),每个内存单元都有地址,所以变量也是有地址的。

2.指针变量和指针和地址关系

我们对整形变量已经变成熟悉,整形变量是存放整形的数据的变量,指针前面已经说了,指针就等于地址,而指针变量当然是存放指针的变量,而因为指针等于地址,则指针变量就是存放地址的变量

int * p=&a这里指针变量p可以存储整型变量的地址p类型为 int*这里&a为整型变量的地址的类型也为 int*

既然存储了地址那这么将地址的内容取出来呢,这里就涉及到一个解引用操作符 * 在指针变量前面加上一个 *就可以取出地址对应的内存单元的内容取出来。

3.指针的大小和类型

指针就是地址,而地址又有地址线产生,则地址的大小取决于是多少位的平台,若是32位的系统则地址也是32位的二进制数则大小就是4个字节,若存储一个地址则需要4个内存单元。若是64位的系统则地址也是64位的二进制数则大小就是8个字节,若存储一个地址则需要8个内存单元。
这里以32位系统为例

这里不同指针类型大小都为4个字节,因为只要是地址不管什么类型就是4或8的字节的大小,但是为什么要分不同的指针类型呢。

 char* 类型的指针变量是为了存放 char 类型变量的地址。short* 类型的指针变量是为了存放 short 类型变量的地址。 int* 类型的指针变量是为了存放 int 类型变量的地址。

1.指针的类型决定了,对指针解引用的时候可以操作几个字节。
int *类型的指针变量

char *类型的指针变量

int *类型的指针变量解引用,一下可以操作4个字节将4的字节的内容全部变成0,而char *类型的指针变量解引用,一下可以只能操作1个字节让一个字节的内容变为0。

2.指针的类型决定了指针向前或者向后走一步地址变化的大小。

int *类型的指针变量加一向后偏移了4个地址,也就是跳过了4个字节内存单元int *类型的指针变量加一向后偏移了1个地址,也就是跳过了1个字节内存单元

用指针给数组赋一个初值

如果你用char*类型的指针一次只能改一个字节

4.指针的运算

指针可以加减整数
指针关系运算(比较大小)

  • 指针减指针
    前提条件:必须要在内存中连续存储的元素,比如数组

*指针减指针的值为 (指针-指针)/sizeof(type ),等于元素个数的偏移量。

也就是说从下标为0的元素到下标为9的元素,偏移量为9

指针减指针实现strlen函数

//指针减指针实现strlen函数int My_Strlen( char *str){ char *start=str;//存储初始地址 char *end=str;while(*end!= '\0')//找到'\0'地址{end++;}return end-start;//两个地址中间元素的个数}int main(){char ch[]="hello";printf("%d\n",My_Strlen(ch));return 0;}

5.二维指针

不就是指向一维指针的指针嘛

int main(){int a=10;int* pa=&a;int** ppa=&pa;printf("%d\n",**ppa);return 0;}

  • 三维指针
int main(){int a=10;int* pa=&a;int** ppa=&pa;int*** pppa=&ppa;printf("%d\n",***pppa);return 0;}


后面四维五维…一样的套路

6.字符指针

char* 类型的指针变量是可以存放 char 类型变量的地址。


  • 字符串指针
    “abcdefg”本质是首字符的a的地址

    像”abcdefg”这种字符串是常量字符串 类型为 const char * ——>字符串的内容不能被修改

    看一道面试题

7.指针的类型,与指向元素的类型

  • 变量地址的类型

    这里看不懂没关系,先往后看,看完了在回来你就懂了

二.野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址, 意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的

1.野指针怎么来

  • 指针未初始化
  • 指针的越界访问
  • 指针指向的空间释放

2.如何规避野指针

1.指针初始化
2.小心指针越界
3.指针指向空间释放即使置NULL
4.避免返回局部变量的地址
5.指针使用之前检查有效性

三.指针与一维数组(要理解后面这里很重要)

1.数组名

  • 数组名是首元素的地址

有两种情况例外:

  • &arr-数组名代表整个数组,取出的是整个数组的地址
  • sizeof(arr)-数组名代表整个数组,计算的是整个数组的大小(字节)

    这里在来验证一下&arr 到底是不是整个数组的地址,如果是那&arr的数据类型是什么

    结论:&arr是整个数组的地址,而且它的类型为 int (*)[10]—>这个类型的指针就是一个数组指针->指向一个数组的地址

2.指针与数组深度理解

首先看一段代码

3.数组的类型

数组的在C语言中没有指定的类型但为了方便理解后面数组指针

数组类型 :去掉数组名剩余的部分int arr[10]; ->类型为int[10]表示数组存储10个整形元素char arr[10];->类型为char[10]表示数组存储10个字符元素int*arr[10];->类型为int*[10]表示数组存储10个整形指针int (*parr[10])(int ,int );->类型为int(*[5])(int ,int )表示数组存储10个函数指针//以此类推

一定要理解下面这张图

四.指针数组

1.指针数组的概念

指针数组–>存放指针的数组

指针数组说到底还是一个数组,既然是一个数组就符合数组的特征
先看一下一维数组的定义方式:

类型说明符 数组名 [常量表达式];int arr[5];


接下来如何定义一个指针数组,很明显要搞定存储元素的类型,既然要存储指针也就是地址,只要搞清楚指针(地址)的类型不就迎刃而解了嘛。
假设我要存储几个整形元素的地址存放到数组里,那这个数组不就是指针数组嘛,而存储的元素类型不就是整形元素地址的类型嘛那不就是int * 嘛就是这么简单。

这里判断到底是数组指针 还是指针数组看数组名与 *结合还是与 [] 结合

  • 变量名与[] 结合 –>指针数组
  • 变量名与 * 结合 –>指针数组

2.指针数组的应用

  • 打印多个数组的内容
    既然指针数组可以存放指针,那将多个数组的数组名首地址都存进指针数组不就很好的解决了嘛。


是不是很像二维数组,先理解这个

五.数组指针

1.数组指针的概念

数组指针–>指向数组的指针

指针数组说到底还是一个指针,既然是一个数组就符合指针的特征
先看一下指针的定义方式:

int* p;–>指向整形变量的指针-指针变量的类型为int * ,存储元素的类型为int char *
char* p;–>指向字符变量的指针-指针变量的类型为char * ,存储元素的类型为char

假设我要定义一个数组指针,指向存储10个整型元素的数组 int arr[10],接下来如何定义一个数组指针,首先我们知道数组指针指向元素是数组,而数组的类型是什么— int [10]
根据我们的逻辑 我们定义出来的数组指针 —> int[10] *parr这样写出来会不会很奇怪 其实正确的写法应该为 int(*p)[10]

用数组指针就可以存储数组的地址,地址就是指针则&arr的类型与数组指针的类型是一致的。

int main(){int arr[10]={1,2,3,4,5,6,7,8,9,10};int (*p)[10]=&arr;return 0;}

如果要储存一个数组的地址(指针),相应的存储该地址的数组指针的类型应该与数组地址的类型都是 int (*)[10]


利用数组指针打印一个一维数组

2.指向指针数组的指针

int *arr [10]->是一个指针数组存放10int*类型的指针

现在我要指针数组的地址&arr存储起来,这时是不是需要一个数组指针来存储。
废话不多说看图


这几张图一定一定要细细品味,这样就不会搞不清楚方向

3.数组指针与二维数组

1.二维数组
二维数组的特点基本与一维数组一致

  • 数组名是首元素的地址

这里二维数组的首元素是一个一维数组是第一行,首元素的地址就是第一行的地址

有两种情况例外:

  • &arr-数组名代表整个数组,取出的是整个数组的地址
  • sizeof(arr)-数组名代表整个数组,计算的是整个数组的大小(字节)
int main(){int arr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};return 0;}

详细分析二维数组

总结:二维数组的数组名是一个一维数组的地址,也就是第一行的地址
*arr就是第1行第1个元素的地址,详情参考上图

2.用数组指针打印一个二维数组

既然二维数组名是一个一维数组的地址,那不就可以用数组指针来存储这个地址。

int arr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};void print1(int arr[3][5], int x, int y ){int j=0;int i=0;for(i=0; i<x; i++){for(j=0; j<y; j++){//printf("%d ",arr[i][j]);//printf("%d ",*(arr[i]+j));//printf("%d ",(*(arr+i))[j]);printf("%d ",*(*(arr+i)+j));}printf("\n");}}void print2(int(*p)[5], int x, int y ){int j=0;int i=0;for(i=0; i<x; i++){for(j=0; j<y; j++){//printf("%d ",p[i][j]);//printf("%d ",*(p[i]+j));//printf("%d ",(*(p+i))[j] );printf("%d ",*(*(p+i)+j));}printf("\n");}}int main(){print1(arr, 3 , 5);printf("\n");print2(arr, 3 , 5);return 0;}


上面用了八种形式表示的第i行第j个元素,其实他们本质都是一样的

六.数组与指针传参

1.一维数组传参

直接看图给你整的明明白白的

一维数组int arr[10]的数组名arr是首元素的地址,这个地址的类型就是 int*,所以用该类型的指针变量接收int*arr。

2.二维数组传参

二维数组int arr[3][5]的数组名arr是首元素的地址是一个一维数组的地址也就是第一行的地址,这个地址的类型就是 int (*) [5] ,所以用该类型的指针变量接收int(*arr)[5]。

3.一级指针传参

4.二级指针传参

5.总结

不管是数组传参还是指针传参,传的都是指针(地址),只要搞明白你要传的地址是什么类型的,然后用对应的类型指针接收即可,就这么简单

七.函数指针

函数指针–>指向函数的指针,用来存放函数的地址,可以用指针变量调用该函数

1.函数的地址

  • 函数的地址->&函数名和函数名都可以表示函数的地址‘’

2.定义一个函数指针

接下来就定义一个存放加法函数的指针

int Add(int x, int y){return x+y;}int main(){int a=10;int b=20;int (*p)(int , int )=Add;printf("%d\n",p(a,b));printf("%d\n",(*p)(a,b));return 0;}

看图就明白的透透的

int (*p)(int , int )=Add; p与 * 先结合说明p是一个指针,将指针变量p去掉剩下的就是指针变量的类型-int(*)(int , int ),说明函数指针指向一个有两个参数类型都是int,返回类型也是int 的函数。

在这来看两段代码看看能不能理解到位

//代码1( *( void (*)() )0 )( );//代码2void ( *signal(int , void(*)(int) ) )(int);

代码1:

代码2

简化第二段代码:
将void(*)(int )类型重命名,定义一个自定义类型

//将void(*)(int)重命名为 pfun_ttypedef void(*pfun_t)(int);int main(){pfun_t signal(int, pfun_t);return 0;}

一定要分清楚,变量名到底与谁先结合,如果是与*先结合那一定是个指针,与()先结合那就是一个函数.

3.函数指针数组

格式:int (*arr[5])(int , int );->arr是个数组,数组有5个元素,每个元素是一个函数指针.

4.用函数指针数组实现简易计算器

不多说直接上代码,就是利用一个函数指针数组,来存储这些功能函数,在选择一个

//函数指针数组int Add(int x, int y){return x+y;}int Sub(int x, int y){return x-y;}int Mul(int x, int y){return x*y;}int div(int x, int y){return x/y;}int Xor(int x, int y){return x^y;}void menu(void){printf("**********1.加法2.减法****************\n");printf("**********3.乘法4.除法****************\n");printf("**********5.异或0.退出****************\n");printf("****************************************\n");printf("****************************************\n");}int main(){int input=0;//定义一个函数指针数组存储各个函数int (*pf[6])(int , int)={0, Add, Sub, Mul, div, Xor};menu();do{int x,y;printf("请选择要进行的运算:");scanf("%d",&input);if ( 0<input && input<6 ){ printf("请输入两个操作数:");scanf("%d%d",&x,&y);//选择一个函数进行计算 printf("%d\n", pf[input](x, y));}else if (input == 0){printf("退出\n");}else{printf("选择错误\n");}}while(input);return 0;}

效果演示

5.指向函数指针数组的指针

直接上代码叭

int main(){int (*parr[5])(int ,int );int (*(*pparr)[5])(int ,int)=&parr;return 0;}

看图详解

八.回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

通俗点就是你将一个函数的地址当做形参传给另外一个函数,另外一个函数既然要接收一个函数的地址则它的函数参数就为一个函数指针,然后通过函数指针去调用原来的函数,则称这个函数为回调函数。

用回调函数实现简易计算器

直接上代码

//函数指针 回调函数int Add(int x, int y){return x+y;}int Sub(int x, int y){return x-y;}int Mul(int x, int y){return x*y;}int div(int x, int y){return x/y;}int Xor(int x, int y){return x^y;}void calc(int (*pf)(int , int )){int x,y;printf("请输入两个操作数:");scanf("%d%d",&x,&y);printf("%d\n",pf(x,y) );}void menu(void){printf("**********1.加法2.减法****************\n");printf("**********3.乘法4.除法****************\n");printf("**********5.异或0.退出****************\n");printf("****************************************\n");printf("****************************************\n");}int main(){int input=0;menu();do{printf("请选择要进行的运算:");scanf("%d",&input);switch(input){case 1:calc(Add);break;case 2:calc(Sub);break;case 3:calc(Mul);break;case 4:calc(div);break;case 5:calc(Xor);break;case 0:printf("退出\n");break;default:printf("选择错误\n");break;}}while(input);return 0;}

这里就不演示实验结果了,与上面的实验结果一致

九.汇总

int arr[5]; arr是一个5元素的整形数组int *p[10];p与[]先结合是一个数组,数组有10个元素,每个元素的类型为int *int (*p)[10];p与*结合是一个指针,指针类型是int(*)[10],数组指针指向一个数组,数组存储10个整形的元素int (* p[10])[5]; p先与[]结合是一个数组,去掉数组名和元素个数p[10],剩下的是数组每个元素的类型为int(*)[5],该类型是一个数组指针指向数组有5个整形的元素。总结:p是一个数组,每个数组的元素是一个数组指针,指针指向了存储5的整形的元素的数组int (*p)(int , int );指向函数的指针,用来存放函数的地址,可以用指针变量调用该函数int (*arr[5])(int , int );arr先与[5]结合是一个数组,去掉数组名和元素个数arr[5],剩下的int(*)(int ,int )是数组每个元素的类型是一个函数指针,总结:arr是一个函数指针数组,数组的每个元素是一个函数指针int (*(*pparr)[5])(int ,int);pparr先与*结合说明是一个指针,去掉数组名pparr剩下的int (*(*)[5])(int ,int)是指针类型在去掉 * 剩下的int (*[5])(int ,int)是指向数组的类型,该数组有5个元素每个元素是一个函数指针总结:pparr是一个指向函数指针数组的指针,指向的数组有5个元素,每个元素是一个函数指针

本文到这就圆满结束啦,文章越到后面越复杂,不过只要细细品味,就能真正玩转指针,如果文章内容对你有帮助的话,赶快收藏点赞起来叭!!!

结束语:
最近发现一款刷题神器,如果大家想提升编程水平,玩转C语言指针,还有常见的数据结构(最重要的是链表和队列)后面嵌入式学习操作系统的时如freerots、RT-Thread等操作系统,链表与队列知识大量使用。
大家可以点击下面连接进入牛客网刷题

点击跳转进入网站(C语言方向)
点击跳转进入网站(数据结构算法方向)