一、前言
- 初始指针(0):着重于讲解指针的概念、基本用法、注意事项、以及最后如何规范使用指针
- 深入指针(1):讲解指针变量常见的类型,如何去理解这些类型、最后就是如何正确的使用
- 深入指针(2):讲解指针较为高阶的用法,理解难度 中度
- 深入指针(3):在(2)的基础上,再讲解其余指针类型的高级用法,理解难度 偏难
- 深入指针(4):指针相关的题目、及答案。理解难度 中度
二、初识指针(0)
1.内存与地址
- CPU(中央处理器),需要向内存读取数据,处理完数据后,还需要将数据放回到内存中,那CPU是如何从内存找到他自己想要的数据呢?
- 现实中,我们去向宾馆申请一间房间,前台的服务人员,会给我们找出一间闲置的空间供给我们使用,并返回给我们一个房卡。那她是如何快速的找到一间闲置的空间呢?那是因为每一间闲置的空间都有编号。当我们不用的时候,她们就会收回这间屋子的使用权。
- 我们将CPU抽象为前台服务人员,把内存抽象为宾馆的房子,把CPU读取数据抽象为去宾馆申请房子,把CPU返回数据抽象为收回这间屋子的使用权......
- 我们就可以推断出,内存也被划分了很多个内存单元,每一个内存单元有自己的专属编码
- 结论:内存被划分为很多个大小为一个字节的内存单元,每一个内存单元都有相对应的内存编码,内存编码又叫做地址。CPU处理数据的时候,则是根据地址去找到所需的数据。
2.指针变量与地址
- 有时候,我们自己要处理数据的时候,也必不可少要用到地址,所以C语言允许我们获得变量的地址,并使用地址,或者通过地址找到该数据,进行使用。
- C语言规定了一个指针变量,专门用来存储数据地址,并且有两个操作符(取地址操作符&,解引用操作符*)。
- 下面是其简单的使用:
- 结论:内存单元编码 == 地址 == 指针
3.指针类型的刨析
i.指针的理解
-
(type) * p;
- p ->p是一个变量
- * ->变量p前面的(*),说明它是一个指针类型的变量
- type->说明了指针变量p,所指向对象的类型
ii.指针在空间的大小
- 了解过音乐的都知道,音乐有这几个音阶(do、re、mi、fa、sol、la、si),在钢琴制作的时候,也没有把哪个键标记下音阶,那演奏家是如何找到音阶的吗?那是因为音阶的分布是被死规定过的,且是公认的,无论哪几种钢琴,它的音阶所在按键位置是不变的。
- 事实上,内存单元的编号也被按照某种标准的规定所设定,计算机中存在地址线,正电流表示二进制1,负电流表示二进制0;在32位平台上,由32根地址线的表示一个内存单元的地址,也就是一个整型,在64位平台上,由64根地址线的表示一个内存单元的地址,也就是两个整型。
- (int)类型在内存中占据4个字节,但我们取它地址的时候,常用第一个内存单元的地址,作为该int类型的地址。
- 小结:在32位平台上,地址由32比特位构成,也就是1个(int)类型的大小,4个字节;在64位平台上,地址由64位比特位构成,也就是2个(int)类型的大小,8个字节。无论什么类型的指针,只要是用来存放地址的,其指针类型的字节大小只与平台有关->(4/8)。
iii.指针类型有什么用?
- 这里我就不卖关系了,直接告诉你们结论:
- 指针类型决定着,指针变量解引用后所能操作的空间。
- 指针类型决定着,指针变量+/-n个整数后,指针的地址变化的字节(为指针所指向对象的类型在内存空间占用字节大小的n倍)。
4.const修饰指针
- 关键字const的作用:
- 使变量,具有常量属性->其变量所存储的数据,不可被修改(但它仍算是变量,并非我们常说的常量)
- const在(*)的左边,const修饰的是指针变量所指向的对象不能够被修改。
- const在(*)的右边,const修饰的是指针变量所存储的地址不能够被修改。
5.指针的运算
i.指针+/-整数
- 指针 +/- 整数(n) = 指针当前的值 +/- n*(指针所指向对象的类型在内存中所占用的字节大小)
ii.指针-指针
- 在我日常生活都会用到日期,月份 +/- 天数 = 月份;月份 - 月份 = 天数。
- 如果我们把月份抽象为指针,把整数(n)抽象为天数。我们就能推测出指针 - 指针 = 此整数(n);n的意义是:两个指针之间元素的个数。
- 注:两个指针必须指向同一块空间,比如一个一维数组,一个指针指向数组的首元素,另一个指针指向数组的末元素,结果就是数组的元素的个数。就好比农历的月份 - 阳历的月份没有意义。
iii.指针的关系运算
- 指针的关系运算很好理解,就是指针之前可以进行(<,>,<=,<=)逻辑运算。毕竟地址是有高地址与低地址的,且地址是由32比特位或64比特位表示的。自然就能进行逻辑运算。
6.void* 类型的指针
- (void*)类型的指针 :可接收任意类型的指针,但不可解引用,也不可进行指针的关系的运算。
- void* 指针常用来做函数的参数或返回值,
- 具体案例请看下面的冒泡排序的思想封装一个可以比较任意类型的一组数据的函数。
7.野指针
i.什么是野指针
- 野指针就是一个指针变量指向的空间,或是随机值或已经被释放,总之就是未经操作系统允许的空间,好比你去宾馆开房,房子都回收了,你再去使用是不合法。
ii.野指针产生的情景
- 指针未被初始化:
- 了解过函数栈帧的都知道,局部变量未初始化,其指向的数据是0xcccccc,是随机值。因为VS检查太严格,无法有具体的代码案例。
- 指针访问越界:
- 指针指向了已被释放的空间:
8.如何避免野指针
- 前面我们已经知道了指针越界的三种情况,我们在使用指针的时候如何避免野指针?
- 指针变量要记得初始化。
- 尽量避免使用指针越界的情况。
- 将暂时用不到的指针变量赋值成空指针:NULL。NULL的值为0,内存中,并不是所有的内存是允许我们访问的,会有一部分内存空间留给操作系统。所以访问NULL会之间报错,你用都用不了。这样也能让我们就算有错,也能知道。
- 检测是否为NULL:若能做到上面的操作,则我们使用的指针要么是NULL,要么就是合法的指针。所以在使用指针的使用用if语句作判断,若指针不为NULL,才使用。
三、深入指针(1)
1.二级指针变量
- 二级指针:
- 接收一级指针的指针,(type*) *pp;
- pp是一个变量
- *与pp结合,表示pp是一个指针变量
- (type*) 的类型说明,pp指向对象的类型
- 常用场景在函数传参的时候,参数有一级指针,且一级指针需要被更改的时候,我们就需要传递一个二级指针。
2.字符指针变量
- 字符指针
- char* p;
- p是一个变量
- *与p结合,表示p是一个指针变量
- char说话,p指向的对象是char类型
3.指针数组
- 指针数组:
- 是一个数组,数组的元素是指针;(type*)arr[整型常量N]
- arr先与 [] 结合,表明它是变量arr是数组名,其中整型常量N表示该数组含有N个元素
- (type*)表示该数组每一个的元素的指针类型;
4.数组指针变量
- 数组指针:
- 是一个指针,指向的对象是一个数组;(type)(*p)[整型常量N]。
- p先于(*)结合,表示它是一个指针,因为[]的结合性强于*,所有得用上()。
- 数组的类型是 (type) [整型常量N];
5.函数指针变量
- 函数指针:
- 是一个指针,指向的对象是一个函数;(type) (*p) (type,type)
- p先于(*)结合,表示它是一个指针,
- 后与 () 结合,表面p指向的对象是一个函数。
- 括号里的两个type,表明函数的参数的类型,前面的type,表示函数的返回类型。
- 注:函数名与&函数名,所代表的含义一致
6.函数指针数组
- 函数指针数组:
- 是一个数组,数组的元素的指针,但这里有的特殊,数组的元素是函数指针;
- 带你们分析理解了那么多个类型,这个自主分析
- 代码案例,转移表;下面讲的有。
7.typedef关键字
- 关键字typedef:
- 将一个数据类型重定义,使复杂的类型,可以简单化;
- typedef type NEW_type
- 将类型为type的,重定义为NEW_type,
- 我们可以用type类型创建一个变量,也可以用NEW_type;
- 注:NEW_type与type是完全等价的,与#define宏定义的符合替换不同
四、深入指针(2)
1.一维数组:数组名的理解
- 数组名:
- 一般情况数组首元素的地址
- 除了关键字sizeof后面的数组名,与&数组名
2.二维数组:数组名的理解
- 数组名:
- 一般情况数组首元素的地址
- 除了关键字sizeof后面的数组名,与&数组名
- 二维数组的元素是?
- 二维数组是将每一组一维数组当作它的元素。
- 所以数组的首元素的地址是第一行数组的地址,
3.一维数组传参
4.二维数组传参
5.指针数组模拟二维数组
i.模拟思路
ii.代码实现
6.二级指针模拟二维数组
i.模拟思路
- 注:int*类型的元素,是连续的,常用的情景是动态开辟空间模拟二维数组。
ii.代码实现
五、深入指针(3)
1.转移表
- 转移表就是函数指针数组;将所需要的函数的指针,存储在一个数组中,通过数组得下标去调用函数,这样就能避免代码的冗余。使得代码又清晰,又简化。
-
//转移表代码实践->计算器功能得实现 #include<stdio.h> void menu() {printf("************************\n");printf("**** 0.exit 2.sub ****\n");printf("**** 1.Add 3.Mul ****\n");printf("**** 4.div 5.Mor ****\n");printf("************************\n"); } 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 Mor(int x, int y) {return x % y; } typedef int(*Fun[])(int, int); int main() {Fun pf = { 0, Add,Sub ,Mul,Div,Mor };int input = 0;do {menu();printf("请下指令:>");scanf("%d", &input);if (input <= 5 && input >= 0){int m, n;printf("请输入两个操作数:>");scanf("%d%d", &m, &n);int ret = pf[input](m, n);printf("%d\n", ret);}else{(input == 0 ? printf("关闭计算机\n") : 1);continue;}} while (input);return 0; }
3.回调函数
- 回调函数就是⼀个通过函数指针调⽤的函数。
如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应
4.qsort函数
- 排序神器——qsort函数之详解-CSDN博客
- 此链接是专门讲解得qsort函数的功能以及用法
- 快速排序【全方面讲解快排,此文足以彻底扫盲】-CSDN博客
- 此链接则是针对qsort函数内部的代码实现逻辑进行讲解——快速排序
5.回调函数小试牛刀---冒泡排序模拟qsort的功能
- 冒泡排序进阶之模拟qsort排序函数(回调函数用法讲解)-CSDN博客
六、深入指针(4)
1.关键字sizeof
- sizeof:
- 关键字
- 计算变量的类型在内存中所占用字节的大小,也就是说它括号里面的式子不参与计算,仅仅判断一下最终结果的类型,然后计算该类型在内存中所占用字节的大小,后返回回来。
2.库函数strlen
- strlen:
- 库函数,使用的时候,需要包含<string.h>头文件
- 计算字符串'\0'之前的元素个数,也就说,strlen的停止条件是遇到'\0';所以strlen只能计算字符串的长度。
3.数组名含义的回顾【一维】
- 数组名:
- 一般情况数组首元素的地址
- 除了关键字sizeof后面的数组名,与&数组名
4.数组名含义的回顾【二维】
- 数组名:
- 一般情况数组首元素的地址
- 除了关键字sizeof后面的数组名,与&数组名
- 二维数组的元素是?
- 二维数组是将每一组一维数组当作它的元素。
- 所以数组的首元素的地址是第一行数组的地址,
5.指针相关习题未完待续
- 此文章发布之后,一个月内必更新~