当前位置: 首页 > news >正文

深入理解C语言指针(三)


  • 点击表格内对应链接跳转对应内容⬇️⬇️⬇️
作者主页吃透C语言专栏Gitee仓库

文章目录

  • 一,字符指针变量
    • 1.与字符的搭配
    • 2.与字符串的搭配
      • (1)字符串详解
      • (2)字符数组或者常量字符串的使用
        • (1)字符数组的使用
        • (2)常量字符串的使用
  • 二,数组指针变量
    • 1.概念
    • 2.使用
  • 三,二维数组传参的本质
  • 四,函数指针变量
    • 1.定义
    • 2.用法
  • 五,typedef关键字
  • 六,函数指针数组
    • 1.转移表

一,字符指针变量

  • 定义:用来存放字符地址的指针变量

  • 指针变量类型为字符类型,权限是一个字节(一个字符大小),所以加减整数或者解引用的权限就是一个字节大小

  • 语法:char* 变量名
char*p
  • 解释:p 是变量名,* 代表这是一个指针变量,变量里面存放的是一个地址,char 代表地址指向的数据是一个字符

1.与字符的搭配

  • 存一个字符的地址:⬇️⬇️⬇️
#include<stdio.h>intmain(){charb='a';char*p=&b;printf("b = %c",*p);return0;}

  • 用字符指针变量存储一个字符的地址,通过解引用*访问这个地址对应的空间,权限是一个字节(一个字符大小)

2.与字符串的搭配


(1)字符串详解

  • 字符串:以"\0"结尾的一串字符。

  • 重点:❗❗❗⬇️⬇️⬇️
  • C语言没有单独的字符串类型,只有字符类型(char)

  • 如果要初始化(实现)一个字符串的话只有两种办法:⬇️⬇️⬇️
  • ❗1.用字符数组来初始化一个字符串
chararr[]="abcdef";//字符数组初始化一个字符串
  • 字符数组即是一个每个元素都是一个字符组成的数组,那每一个元素的空间就可以用来存放一个字符串的一个字符,最后一个空间存放"\0",这样通过字符数组就可以实现和初始化一个字符串

  • 这时候的字符是初始化在一个变量中的,和常量字符串的属性没有关系和联系

  • 是先开辟了一个变量,字符数组,然后把字符串存了进去,这时候的字符串存放在了一个变量中,数组的每一个元素都是一个字符,用来存放字符串的每一个字符
  • ❗2.直接写一个字符串(字符串常量)
intmain(){"abcd";return0;}
  • “字符串”,在编译器中会转化为字符串首字符的地址(❗❗这时的字符串是常量字符串)

  • 如果直接拿”字符串“用的话,使用的就是字符串首字符的地址。(❗❗这时的字符串是常量字符串)

  • 直接写一个字符串,就相当于实现和初始化了一个字符串这个字符串没有放在变量里面,是直接创建并且初始化在内存中的,这个字符串是不能修改的,因为这是一个字符串常量,没有放入变量中。有人说我把"abcd"改成"bd"这不就是修改吗,不是!是重新实现和初始化了一个字符串常量,之前的那个字符串常量已经没了,空间被释放了。

  • 常量字符串是只读的,不可以被修改的。

  • &(取地址操作符)操作的对象是变量,无法对常量取地址,&“abcd”,在语法上都是错误的
#include<stdio.h>intmain(){printf("%c",*"abcdef");return0;}


(2)字符数组或者常量字符串的使用


(1)字符数组的使用
  • 字符数组就是一个数组,只是里面的内容是一个个字符而已,这一个个字符组成了一个字符串
#include<stdio.h>intmain(){chararr[]="abcd";printf("arr首元素为 = %c\n",*arr);printf("arr = %s",arr);return0;}

  • 字符数组就是元素由字符构成的数组,所以用法和数组是一样的用法。

(2)常量字符串的使用
  • 常量字符串的重点:⬇️⬇️⬇️
  • 1:常量只读不修改,无法使用&(&只能取变量的地址,遇到常量报错)

  • 2:常量字符串在编译器中自动转化为常量字符串首字符的地址(常量字符串 == 首字符地址)

#include<stdio.h>intmain(){"abcd";printf("%c\n",*"abcd");printf("%s","abcd");return0;}

#include<stdio.h>intmain(){"abcd";char*p="abcd";printf("%c\n",*p);printf("%s",p);return0;}

#include<stdio.h>intmain(){char*p1="abcd";char*p2="abcd";printf("%p\n",p1);printf("%p",p2);return0;}

  • 代码解释❗❗(超级重点)❗❗⬇️⬇️⬇️

  • "abcd"是字符串常量,直接写的话就是直接实现和初始化在可读区,如果我们去使用这些不可改变的常量,用的都是同一个常量,因为常量是不可改变的,是只读的,这两个条件让相同的常量在内存中有且只有一个,这就是为什么两个"abcd"打印出它们首字符地址的时候,是一样的地址的原因了,因为从始至终都是同一个常量

  • 变量是开辟一个空间,里面的内容可以随时改变,但是常量直接写就行了,不需要开辟出一块空间放入进去,而是直接写入内存之中,所以这种只读的常量不需要创建多个,一个足够了,相同的常量永远都是那一个。

  • 写 “abcd”,不是你创建了一个常量,而是编译器给你 “偷偷造了一个全局只读数组,然后把它的地址给你用”。

  • "abcd"在编译器里面转化成字符串首字符的地址,所以* “abcd”,得到的就是字符串的首字符a,用%c占位符把a跳转到前面就打印出来了a

  • ❗❗重点%s的用法和语法❗❗⬇️⬇️⬇️
  • %s 作为一个占位符,读取的是一个字符的地址,然后通过这个地址读取一个字符大小的内容,然后继续往后地址+1读取下一个地址访问对应的一个字节大小的空间,连续访问直到遇到 \0 就停止访问。

  • 1:如果你放的是一个整型或者别的类型,%s不会管这么多,只会把这个地址当作一个字符的地址去访问

  • 2:通过这个地址,只访问一个字节大小的空间(也就是一个字符大小),把访问到的所有数据当作ascll码值转化为对应的字符让printf()进行输出,如果遇到的数值不是ascll值所对应的字符,则输出乱码

  • 3:内存中的所有数据都是以二进制的形式存储的,字符,符号,标点,都有对应的ascll码值,所以写出来的所有的代码,语法都是给人看到,到了最后,这些所有的代码都是由字符,符号,标点组成的,都会变成对应的二进制。

  • 所以%s读取的内容一定是一个二进制,但是ascll码值的范围是0到127,然而一个字节可以包含的大小是0到255,所以一旦读取的内容不在ascll码值范围里面,就会打印乱码,如果遇到0,就被当成 /0 直接停止打印

  • 这就是为什么%s可以通过字符串的首地址就把整个字符串的内容打印出来的原因

二,数组指针变量

1.概念

  • 定义:一个指针变量,变量的类型是数组

  • 解释:数组指针变量里面放的是一个数组的地址,通过这个地址访问的是一个数组,权限是一个数组的大小

  • 语法:⬇️⬇️⬇️
int(*p)[5]
  • 解释:⬇️⬇️⬇️
  • *代表这是一个指针变量

  • p是变量名

  • ()必须把(*p)包含在里面,因为这样才能代表是一个指针变量,不然int * p[5],*不会自动和p结合,这样就变成了一个整型指针数组了,p是数组名,数组有5个元素,每个元素是 int* 类型的地址
  • [ ]的优先级高于*号

  • int 代表指针变量里面地址所访问的数组是一个整型的数组

2.使用

  • &数组名(arr),这时候的数组名代表的是整个数组,所以&取出来的是整个数组的地址
  • 代码演示⬇️⬇️⬇️
#include<stdio.h>intmain(){intarr[]={1,2,3,4,5};int(*p)[5]=&arr;return0;}

  • &arr取出来的是整个数组的地址,但是&取的是数组的起始地址,也即使数组首元素的起始地址

  • 为什么无法用printf()来验证和打印整个数组:⬇️⬇️⬇️

  • 之前就说过,printf只会打印“ ”内的原值,不会计算,除非使用占位符,才能让这个值可以随时改变,但是c语言中没有数组的占位符,所以即使取出来了一个数组的地址也无法通过printf直接把整个数组打印出来,只能把这个数组的每一个元素一个一个遍历,才能打印出来这个完整的数组

  • 所以即使p里面放的是一个整型数组的地址,这个指针变量的权限是整个数组,我们也无法一口气打印这个数组,因为没有数组的占位符

  • 即使有了整个整形数组的地址,但是如果*&arr的话,*和&会相互抵消,所以最后只剩下arr,单独一个数组名代表的是数组首元素地址的意思

  • int(*p)[5] = &arr,我拿到了这个数组的地址,那*p得到的就是整个数组,&取出来的是数据的起始地址,也就是数组首元素的起始地址,但是p的类型是int*[5],解引用的权限是整个数组的大小,所以*p就可以访问整个数组的内容,但是无法打印出来,因为没有c语言中没有数组占位符

  • 总结:也就是说&arr拿到了整个数组的地址,这个地址的类型是整个数组所以是得到了整个数组的大小的权限,所以通过这个地址理论上是可以直接访问整个数组的内容的,但是如果直接*&arr的话,*和&在语法上会相互抵消,最后拿到的只是arr也就是数组首元素的地址,但是如果我们把&arr放在指针变量里面的话,这个指针变量类型和数组对应的话,那可以通过指针变量访问到整个数组的内容,这就是我们之前说的拿到了整个数组的地址,权限是整个数组,所以加减整数跳过的也是整个数组的大小,访问的也是整个数组的空间,但是*p虽然可以访问整个数组的空间,但是c语言中没有数组的占位符,所以不能直观的打印出来,但是调试的时候可以看的到拿到了整个数组的内容。

三,二维数组传参的本质

  • 定义: 首元素的地址(首个一位数组的地址)
  • 解释:⬇️⬇️⬇️

  • 二维数组是每一个元素都是一位数组的数组,每个元素都是一维数组,组成的数组就是二维数组

  • 数组名代表的是数组首元素的地址,那二维数组的数组名代表的也就是二维数组首元素的地址,二维数组的首元素是一个一位数组,那二维数组的数组名代表的就是一个一维数组的地址(数组指针)

  • 这个地址的类型是数组类型的地址,权限是一个数组大小

  • 二维数组传参,传的是数组名,也就是首个一维数组的地址(首元素的地址)(数组指针)

  • 所以二维数组传参的本质就是传了一整个一维数组的地址(数组指针)
  • 代码解释:⬇️⬇️⬇️
#include<stdio.h>voidtest(intarr[3][5],inta,intb){for(inti=0;i<a;i++){for(intj=0;j<b;j++){printf("%d ",arr[i][j]);}printf("\n");}}intmain(){intarr[3][5]={{0,1,2,3,4},{1,2,3,4,5},{2,3,4,5,6}};test(arr,3,5);return0;}

  • ⬆️⬆️⬆️这是我们之前的写法,把二维数组以及行和列一起传过去,通过下标依次遍历访问二维数组中每一个一维数组中的元素
  • 现在了解了二维数组名的本质,我们可以试试新的写法
  • ⬇️⬇️⬇️
#include<stdio.h>voidtest(int(*p)[5],inta,intb){for(inti=0;i<a;i++){for(intj=0;j<b;j++){printf("%d ",p[i][j]);}printf("\n");}}intmain(){intarr[][5]={{0,1,2,3,4},{1,2,3,4,5},{2,3,4,5,6}};test(arr,3,5);return0;}

  • 解释:⬇️⬇️⬇️
  • 既然二维数组所代表的是首元素的地址,也就是一个一位数组的地址,那根据这个一维数组的类型就可以写出来形参的数组指针了

  • arr 是一个整形数组为元素组成的二维数组,那么首元素地址的类型就是一个整形数组指针,也就是int (*p) [5]

  • p [i] [j] :之前解释过,[ ]下标访问操作符在编译器中会转化为*(地址+整数),也就是对[ ]的两个操作数相加然后解引用

  • 既然p存的是一维数组的地址,且p的类型为int (*)[ 5 ],那么*解引用的话可以得到(访问)整个数组,得到了整个数组以后相当于arr1(整个首一维数组)[j],arr1[j],又相当于这个一维数组首元素的地址通过下标引用操作符和引用操作符内的整数相加然后解引用,也就是*(arr1 + j),这样就可以拿到这个一维数组里面的元素,通过p [i] [j],也实现了遍历数组

四,函数指针变量

1.定义

  • 一个指针变量,类型是函数的类型,存的是一个函数的地址

  • 通过这个指针变量存储的地址解引用的权限是一个函数大小

  • 函数地址:⬇️⬇️⬇️
  • 1:单独写一个函数名代表的是整个函数的意思(这是一个非常空虚的概念,用函数的时候只要通过地址就可以直接找到这个函数的空间进行访问,通过这个地址就可以调用函数,所以在几乎所有的情况下,函数名代表的都是函数地址的意思,为了人们理解,函数名代表整个函数,但是实际上就是函数地址,从逻辑和底层的角度都是最方便最合理的)

  • 2:但是在合法表达式中,函数名会被隐式转化为函数的地址,但是sizeof和_Alignof表达式中对函数名进行操作的话,C语言是不允许的,无法编译,所以属于错误表达式,但是其他正常的表达式中,函数名就是函数的地址

  • 3:&函数名得到的是函数的地址,这里要说明的是&的操作数必须是一个左值(可以被 = 赋值)(变量,数组,结构体成员),所以&函数名这种写法是错误的,但是C语言规定了这种写法是允许的,得到的结果就是函数的地址
  • 函数指针变量语法:⬇️⬇️⬇️
  • 返回值类型 (*指针变量名)( 参数类型列表) ;
int(*p)(int,int)
  • 解释:p是函数指针变量名

  • *代表这是这是一个地址

  • int 是函数的返回值类型

  • (int int )是函数参数类型

  • 注意事项:❗❗❗
  • int (*p) (int int )中的(*p)是必须用()括起来,强调代表这是一个指针变量*代表着是指针变量的意思,如果不括起来的话int *p (int int),*不会自动和p结合,这就变成了一个函数声明,函数名是p,返回值类型是int *,参数类型是(int int )

2.用法

  • 调用函数:⬇️⬇️⬇️
  • 在表达式中函数名会隐式转化成函数地址,所以函数名(),在函数调用的表达式中,函数名隐式转化成了函数的地址,所以用一个函数的地址就可以调用这个函数
  • 代码⬇️⬇️⬇️
#include<stdio.h>intadd(inta,intb){returna+b;}intmain(){int(*p)(int,int)=add;printf("3+4的结果是 = %d\n",add(3,4));printf("3+4的结果是 = %d\n",p(3,4));printf("3+4的结果是 = %d\n",(*p)(3,4));printf("3+4的结果是 = %d",(&add)(3,4));return0;}

  • 代码解释⬇️⬇️⬇️
  • 1:add(3,4)函数名在函数中隐式转化为函数地址,直接调用函数

  • 2:函数指针变量 p 存的 add 函数的地址,所以用p(函数地址),可以直接调用 add (函数)

  • 3:(*p),得到的是这个函数,但是函数出现在表达式中又会马上隐式转化为函数的地址,最终还是通过函数的地址调用函数

  • 4:&add得到的是函数的地址,直接通过函数的地址调用函数

  • 5:所以在函数调用中,函数名只是一个语法形式,最终还是通过隐式转化成函数地址去找到这个函数的空间进行调用和访问

  • 6 :补充:函数开辟空的方式是函数栈帧,向内存申请了一块临时的空间,这块空间的起始地址,又一个寄存器保存,所以调用函数的时候,直接通过这个地址找到这个函数的空间是最方便的

  • 7:函数名只是一个叫法,虽然函数名代表的是这个函数,但是在表达式中会隐式转化成函数地址,但是函数在用的时候,就是函数的地址才有用,函数名的意义呢,代表整个函数的意义在哪里呢,最后有用的就是这个地址,所以有些理解是我们人类语法的理解。

五,typedef关键字

  • 定义:typedef是用来类型重命名的,可以将复杂的类型简单化。

  • 语法:typedef 原类型 别名
typedefunsignedintuint;//将unsigned类型 重命名为uinttypedefint*ptr_t;//将int* 类型 重命名为ptr_t
  • 注意❗❗❗
  • 原类型是带 * 号的话,别名的位置要放在 * 的旁边,也就是放在变量名的位置
typedefint(*ptr_t)(int,int)
  • 原本没有typedef的话,ptr_t 就是一个函数指针变量变量名,但是有了 typedef 的话,这个变量名位置就是 typedef 定义类型的别名

六,函数指针数组

  • 定义:一个数组,数组内的每一个元素都是函数指针(一个地址,指向的内容是函数)

  • 语法:返回值类型 (*数组名 [数组大小] ) (参数类型列表) ;

  • 解释:函数指针数组的数组名要写在原本函数指针变量名的位置,而 数组大小依然紧跟在数组名后面。
int(*p)(int,int);//函数指针变量int(*arr[5])(int,int);//函数指针数组
  • 原本函数指针变量p的位置变成了( 数组名 + 数组大小 )( arr[5] )
  • 解释:
  • arr是函数指针数组的数组名

  • [5]是数组的元素个数

  • int (*) (int,int)是函数指针的类型

1.转移表

  • 函数指针数组的作用之一,实现一个转移表
  • 定义:转移表是一个函数指针数组,每个元素(函数指针)指向一个函数。程序运行时,通过索引(数组下标对应选择的元素)来调用对应的函数。
  • 计算器的一般实现:
#include<stdio.h>intadd(inta,intb){returna+b;}intsub(inta,intb){returna-b;}intmul(inta,intb){returna*b;}intdiv(inta,intb){returna/b;}intmain(){intnum=0;do{printf("*****************************************\n");printf("******1.加法 2.减法 ***********\n");printf("****** ***********\n");printf("****** ***********\n");printf("****** ***********\n");printf("******3.乘法 4.除法 ***********\n");printf("******0.退出 ***********\n");printf("*****************************************\n");printf("请选择运算_>");scanf("%d",&num);switch(num){case1:{inta=0;intb=0;printf("请输入要计算的两个数字_>");scanf("%d %d",&a,&b);printf("%d\n",add(a,b));break;}case2:{inta=0;intb=0;printf("请输入要计算的两个数字_>");scanf("%d %d",&a,&b);printf("%d\n",sub(a,b));break;}case3:{inta=0;intb=0;printf("请输入要计算的两个数字_>");scanf("%d %d",&a,&b);printf("%d\n",mul(a,b));break;}case4:{inta=0;intb=0;printf("请输入要计算的两个数字_>");scanf("%d %d",&a,&b);printf("%d\n",div(a,b));break;}case0:{break;}default:{printf("输入错误请重新输入_>");break;}};}while(num);return0;}

  • 这个普通的计算器里面我们用了很多重复的代码,是代码非常不美观不简洁,容易出错
  • 我们可以使用转移表来实现一下(函数指针数组)⬇️⬇️⬇️
#include<stdio.h>intadd(inta,intb){returna+b;}intsub(inta,intb){returna-b;}intmul(inta,intb){returna*b;}intdiv(inta,intb){returna/b;}intmain(){intnum=0;do{printf("*****************************************\n");printf("******1.加法 2.减法 ***********\n");printf("****** ***********\n");printf("****** ***********\n");printf("****** ***********\n");printf("******3.乘法 4.除法 ***********\n");printf("******0.退出 ***********\n");printf("*****************************************\n");printf("请选择运算_>");int(*sum[55])(int,int)={0,add,sub,mul,div};scanf("%d",&num);if((num>=1)&&(num<=4)){printf("请输入操作数_>");inta=0;intb=0;scanf("%d %d",&a,&b);printf("%d\n",sum[num](a,b));}elseif(num==0){printf("退出计算器");}else{printf("输入错误,请重新输入");}}while(num);return0;}

  • 用一个函数指针数组存入所有的函数地址,再用统一用数组下标去访问这些函数指针,这些函数指针再调用对应的函数实现对应的计算器功能。

  • 点击表格内对应链接跳转对应内容⬇️⬇️⬇️
作者主页吃透C语言专栏Gitee仓库

http://www.jsqmd.com/news/816922/

相关文章:

  • 【IE大纲】工业工程工程师知识框架
  • 在hermes agent项目中配置custom provider指向taotoken的完整流程
  • 源德广告是做什么的?在普宁做了多少年了?|品牌介绍与服务概览 - 掌上普宁品牌观察
  • CATIA多实体零件自动化拆分:pyCATIA解决复杂几何体管理的技术挑战
  • 乌鲁木齐黄金回收“报价即结算价”实体店有哪些?实测发现一家靠谱选择 - 新闻快传
  • C# 三层架构
  • 《Java面试85题图解版(二)》进阶深化上篇:并发编程 + JVM
  • C++ AVL树的学习
  • 【CanMV K210】显示交互 触摸屏画图与 LCD 轨迹绘制
  • Python MongoDB客户端实战:PyMongo深度解析
  • 米立特国产移液器全系解析:覆盖科研与工业领域的精准移液工具 - 品牌推荐大师
  • WechatDecrypt终极指南:安全高效解密微信聊天记录的完整方案
  • 避坑指南:STM32的OSCIN/OSCOUT引脚配置为GPIO后,如何保证系统时钟稳定运行?
  • 桥接模式和NAT模式
  • 2026北京婚姻纠纷找律师事务所:专业靠谱怎么选?这份参考请收好 - 产业观察网
  • 【逻辑设计】卡诺图化简实战 | 从真值表到最简电路 | 利用无关项优化设计
  • 北京翡翠变现攻略:翡翠手镯、挂件回收,专业鉴定无隐形扣费 - 奢侈品回收测评
  • AGV机器人48V锂电池选型指南:特种定制能力决定供应商质量 - 新闻快传
  • 从模拟信号到云端可视化:光敏电阻物联网项目全链路实践
  • 量子通信与6G融合:探索未来通信新维度
  • 新闻发布行业核心服务商技术盘点 多维度拆解适配逻辑 - 奔跑123
  • AntiDupl.NET:智能图片去重工具,轻松释放硬盘空间
  • 谷歌发布AI语音听写功能Rambler,集成Gboard支持语码切换,今夏率先登陆部分安卓机
  • 《Java面试85题图解版(三)》上篇:高阶架构设计篇
  • 【亲测门店】兴化市别墅品牌对比,哪家更靠谱? - 花开富贵112
  • 运维人会被 AI 淘汰吗?未来的机房,可能连值班都不需要了
  • 探索Taotoken模型广场如何帮助我根据任务选择合适的大模型
  • 2026年餐饮品牌扩张发展背景下的适配性餐饮SaaS服务商专业分析与推荐 - 产业观察网
  • 仓储物流机器人48V电池定制周期多久?哪家厂家值得合作?——以浩博电池为例 - 新闻快传
  • 视频硬字幕提取:本地化AI如何破解87种语言的视频转录难题