一、指针是的含义与指针运算?
1、指针的含义:内存世界的 "寻宝地图"🗺️⚡
想象你住在一个巨大的小区里,每栋房子都有一个门牌号(比如 0x7ffee),房子里住着不同的数据——可能是数字、字母、甚至是一整个蛋糕的配方!🍰
但问题是——你不可能把所有东西都背在身上,于是你发明了一种神奇的 "小纸条",上面只写门牌号。这张小纸条,就是 “指针”!
---
🌰 举个栗子:
- `&宝藏`:就是问小区保安:"宝藏住在哪?",保安回答:"0x7ffee!"
-藏宝图`:拿着小纸条找到房子,踹门大喊:"把里面的东西交出来!"——于是你拿到了42。
---
🎮 指针的骚操作:
1. 偷看邻居:
`藏宝图 + 1` → 纸条上的地址+1,偷看隔壁房子(可能是垃圾也可能是惊喜!🗑️)
2. 连环追踪:
如果房子里藏着另一张小纸条(**指针的指针**),就能玩套娃寻宝!🤯
3. 空指针危机:
纸条写"这里没房子"(`NULL`),你一脚踹过去——程序崩溃!💥
---
🤔 为什么要用指针?
- 省力气:搬动大象(大块数据)时,你只需传递"大象住在哪"的小纸条,不用扛着大象跑!🐘
-隔空改物:把纸条传给朋友,朋友也能往房子里塞新东西!(函数修改外部变量)
---
📜 经典口诀:
> `&` 是问地址,`*` 是挖宝藏
> 指针玩得溜,内存任你浪!
现在,试试对你家的冰箱门牌号写个指针?🚪(危险行为,程序员专属娱乐)
2、创建变量
变量的创建的本质其实是在向内存申请空间,内存单元的编号==指针==地址
编译器是通过地址找内存单元的。
在这里,pa是一个指针变量,这个变量是用来存放地址的,pa叫做指针变量, 这个变量是用来存放地址的, pa的类型是int *,*表示pa是指针变量,int表示pa指向的变量a的类型是int。‘ * ’是解引用操作符,也叫间接引用操作符。
指针变量是用来存放地址的,地址的存放需要多大的空间,那么指针变量的大小就是多大。
指针变量的大小和类型是没有任何联系,只和平台有关。
3、void * 指针
void*类型指针是一种无具体类型的指针,它可以接受任意类型的地址,不能进行+ 、-运算,和解引用运算。
4、指针运算
📍 指针运算:内存世界的 "跳格子游戏" 🕹️
指针运算就像在内存的街道上玩 **跳格子**,但规则很奇葩——**每一步能跳多远,取决于你带的是什么行李**(数据类型)!
---
🌟 三大核心玩法:
1. 加减法:跳格子!
指针 `+ N` 不是简单的数学加法,而是 “按行李大小跳格子”:
- `char*’每次跳 1字节(轻装上阵)
- `int*` 每次跳 4字节(扛着大箱子)
- `double*` 每次跳 8字节(搬家卡车)
2. 减法:测距离!(在指针-指针 俩个指针)
两个指针相减,算出 “它们之间隔了多少个行李位”:
3. 比较:谁在前?
用 `>`, `<`, `==` 比较指针,就像比谁的门牌号更大:
🚨 危险行为警告:
- 野指针漂移:跳到不属于你的内存区域——程序崩溃!💥
- 类型混乱:`int*` 和 `char*` 混着算——结果让你怀疑人生!🤯
- 数组越界:以为自己还在街上,其实掉进阴沟!🕳️
5、野指针
指针指向的位置是不可知的主要有以下几种情况:
(1)指针未初始化
这就是没有初始化的指针,
(2)指针越界访问
当指针的指向范围超出数组arr的范围时,p就是野指针,举例如下:
(3)指针指向的空间释放
p一旦拿到这个地址,就是野指针因为p中存放的这个地址指向的内存空间已经还给操作系统了。
(指针指向的内存空间不属于当前程序,这个时候就是野指针)
🌰 实战栗子:遍历数组
(4)如何规避野指针?
1、如果不知道指针变量指向哪里时,那么就初始化为NULL。
2、小心指针越界,指针变量在使用时,及时置于空指针,指针在使用前,检查有效性。
6、assert断言
assert()
assert(p!=NULL);验证p是否等于NULL,如果确实不等于NULL,程序继续运行,否则会终止运行,并且给出报错提示。表达式为真返回值非零,如表达式为假,就会返回零,assert()会报错,assert()不会产生任何作用,程序继续运行。
注:🤔 为什么要学指针运算?
- 高效访问连续数据(数组、字符串)
- 手动操控内存布局(高级玩法如动态内存池)
- 理解底层如何工作(装13必备技能)
📜 灵魂总结:
> 指针加减按类型,
> 比较减法测距离。
>内存街道任你跳,
> 越界一秒变砖机!
现在,试试用指针运算写个循环,但别把程序跳崩了哦~ 😉
二、指针相关的知识点:const修饰指针、传值调用和传址调用
1、const
1、const修饰变量时,这个变量叫做常变量,这个被修饰的变量本质上还是变量,只是不能被修改
2、const修饰指针变量
(1)const放在*的左边: int const*p:限制的是指针变量指向的内容
(2)const放在*的右边:int* const p:限制的是p这个指针变量的内容不能被修改,但是*p是不受限制的吗,也就是说可以通过p来改变p指向的内容。举例如下:
第一种情况:
此时只能样修改p如下:
第二种情况:
这是错误的
应该是:
2、传值调用和传址调用:
定义一个函数指针变量pf,传参给Add函数,返回它们的值最后再打印,这是传值调用
下图是传址调用:
三、二级指针、指针数组
1、数组名的理解
(1)sizeof(数组名):sizeof中单独存放数组名,表示整个数组,计算的是整个数组的大小,单位是字节。
(2)&数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素地址是有区别的)。
除了这俩个特例之外任何地方使用数组名,数组名都表示首元素的地址。
2、二级指针
eg
3、冒泡排序 的实现
冒泡排序的核心思想是俩俩相邻的元素进行比较(只能是整型数组)
目标:升序
思维导图:
具体的代码实现:
4、指针数组
顾名思义指针数组就是存放指针的数组
例如:int* 、 char* 、 double* 、 float*
四、指针变量
1、字符指针变量
存放字符变量的地址,指向字符变量
2、数组指针变量
就是存放数组的地址,指向的是数组
二维数组的数组名也是表示首元素地址的;二维数组是一维数组的数组,每一行是一个元素,首元素就是第一行这个一维数组的地址。
3、函数指针变量
指向函数的指针。
(*pf)(int,int)=&Add
表示:pf是函数指针变量,指向的函数参数俩个类型都是int类型。
上图的例子是由函数指针来进行的,pFunc是函数指针变量,这个变量储存的是函数的地址,是指向函数的指针通过它可以间接调用其所指函数,就像代码中先让pFunc函数指向add函数来执行加法运算,后又指向subtract函数来执行减法运算。函数名也是地址
typedef关键字使用
用来类型重命名,可以将复杂的类型简单化。
定义指针变量时必须这样定义:新的类型名字必须在*的右边
4、函数指针数组
用途:转移表
函数指针数组是数组,数组中存放指针。
例如:int* parr[3];这是一个整型指针数组,里面存放整型指针。
五、回调函数和qsort
1、回调函数
回调函数是通过函数指针(类型是void(*)( 参数))来调用的函数,将函数的指针(地址),作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方来直接调用,而是在特定的事件或条件发生时,由另外一方调用的,用于该事件或条件进行响应。(注:函数名也是地址)
2、qsort函数
在c语言中也有一个函数内部使用了回调函数,qsort排序算法,它的使用需要使用头文件<stdlib.h>;
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
一共有四个参数 ,
- base :指向要排序的数组的起始地址。
- nmemb :数组中元素的个数。
- size :每个元素的大小(以字节为单位)。
- compar :指向一个比较函数的指针,用于比较两个元素的大小。举例如下:
qsort函数的使用范围:
qsort 函数适用于以下范围
1、 各种数据类型的数组排序:可以对 int 、 double 、 char 等基本数据类型的数组进行排序,也能对结构体数组等自定义数据类型进行排序,只要提供合适的比较函数。例如,有一个包含学生信息的结构体数组,可通过 qsort 按照学生成绩或姓名等进行排序。
2、大规模数据排序: qsort 算法平均时间复杂度为 O(nlog n),在处理大规模数据时效率较高,因此适用于对大量数据的数组进行快速排序。
3、需要灵活排序规则的场景:通过自定义比较函数,能够根据不同的需求定义各种排序规则,如升序、降序或按照特定条件进行排序。比如,对字符串数组按照字符串长度进行排序。
六、sizeof和strlen的对比
1、sizeof求的是占用内存的大小,不在乎内存中存放什么数据
2、strlen是c语言库函数,功能是求字符的长度,统计的是'\0'之前的字符个数
这期内容到这里结束了,我们下期再见!!!
有不足之处请提出!!!