安徽建新建设工程有限公司网站,超级外链工具 增加外链中,wordpress模板是什么意思,网络营销八大工具指针 导言一、指针与指针变量二、指针变量的创建和指针类型三、指针类型的意义3.1 指针 /- 整数3.2 指针解引用 四、野指针4.1 定义4.2 野指针的成因4.3 指针未初始化4.4 指针越界访问4.5 指针指向的空间被释放4.6 如何规避野指针 五、指针运算5.1指针-整数5.2 指针-指针5.2.1 … 指针 导言一、指针与指针变量二、指针变量的创建和指针类型三、指针类型的意义3.1 指针 /- 整数3.2 指针解引用 四、野指针4.1 定义4.2 野指针的成因4.3 指针未初始化4.4 指针越界访问4.5 指针指向的空间被释放4.6 如何规避野指针 五、指针运算5.1指针-整数5.2 指针-指针5.2.1 数组下标差值的意义5.2.2 strlen函数5.2.3 字符串5.2.4 strlen函数的模拟实现——寻找\0的数组下标5.2.4.1 函数的返回类型5.2.4.2 函数的参数5.2.4.2函数的实现 5.2.5 strlen函数的模拟实现——指针-指针5.2.6 strlen函数的模拟实现——函数递归 5.3 指针的关系运算 六、指针和数组七、二级指针7.1 二级指针的创建7.2 二级指针的工作原理 八、指针数组8.1 指针数组的创建8.2 指针数组的初始化 九、二级指针与指针数组9.1 二级指针-整数9.2 二级指针-二级指针9.3 二级指针的关系运算9.4 指针与数组名的相互转换9.5 有趣的变形9.6 总结 十、指针数组模拟二维数组十一、void*指针十二、关键字const12.1 变量12.1.1 变量的分类12.1.2 变量的生命周期和作用域12.1.3 变量的优先级 12.2 const修饰局部变量12.3 const修饰指针变量 十三、assert断言13.1 assert工作原理13.2 NDEBUG13.3 assert的优缺点 十四、字符指针变量14.1 字符串和字符数组14.2 常量14.3 常量字符串14.4 总结 十五、数组指针15.1 数组类型15.2 数组类型的变量15.3 数组变量的创建和初始化15.4 数组指针变量的创建与初始化15.5 数组指针与指针数组15.5.1 数组与指针区别15.5.2 数组指针与指针数组的区别15.5.3 数组指针与二级指针15.5.4 数组指针和二维数组15.5.5 总结 十六、函数指针变量16.1 函数名16.2 函数指针变量的创建和初始化16.3 函数指针的使用16.4 关键字typedef16.5 有趣的代码 十七、函数指针数组17.1 函数指针数组的创建17.2 函数指针数组的初始化17.3 函数指针数组的使用 十八、转移表18.1 计算器的模拟实现 结语 导言
大家好很高兴又和大家见面了今天我们终于开始了指针内容的学习了。在开始介绍指针之前我们先回顾一下前面的知识点。 在前面的学习中我们了解了内存以及地址的相关知识点
计算机硬件中的存储器分为主存储器和辅助存储器主存储器就是我们所说的内存在主存储器中主存储器被划分成了一个个小的存储单元这就是内存单元内存单元的大小为1个字节每个内存单元都有自己的编号这些编号就是内存单元的地址内存的工作方式是通过存储单元的地址进行存取的这种存取方式被称为按地址存取地址是由电信号的低电位(0)与高电位(1)组成的我们通过比特位来存放不同的电位一个比特位只能存放一个‘0’或‘1’在32位操作系统中地址总共有32个比特位在64位操作系统中地址总共有64个比特位计算机的单位中除了bit、byte之间的转化为8外其它单位之间的转化都是1024程序猿可以通过取地址操作符将操作对象的地址取出来程序猿可以通过解引用操作符*将地址中存放的值取出来 PS上述知识点在数组、函数栈帧的创建与销毁以及操作符篇章中都有详细介绍 【数组篇章】详细介绍了内存以及地址的表现形式【函数栈帧的创建与销毁篇章】详细介绍了存储器【操作符篇章】详细介绍了计算机中的单位、地址的产生与地址的作用 对这些内容感兴趣的朋友可以点击链接来了解一下相关知识点。 在回顾完这些知识点后我们再来看看什么是指针
一、指针与指针变量
在计算机科学中指针pointer是编程语言中的一个对象利用地址它的值直接指向points to存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元可以说地址指向该变量单元。因此将地址形象化的称为“指针”。意思就是通过它能找到以它为地址的内存单元。
我们知道字符变量是用来存放字符的、整型变量是用来存放整数的、浮点型变量是用来存放浮点数的指针变量也是同样的作用它是用来存放指针的因为指针就是地址我们也可以说指针变量是用来存放地址的。 注意我们在口语中说的指针一般指的是指针变量。 二、指针变量的创建和指针类型
我们知道对于变量的创建是通过数据类型变量名这个格式来实现的变量的初始化会根据变量数据类型的不同给变量赋予一个同类型的初始值如
//变量的创建及初始化
char ch a;
short sh 1;
int i 2;
long l 2;
long long ll 4;
float f 1.0f;
double lf 2.0f;现在我们知道指针变量存储的是指针也就是存储的地址我们可以通过取地址操作符将操作对象的地址取出来赋值给指针变量来完成指针变量的初始化
//指针变量的创建
int a 4;
p a;对于指针变量来说它的数据类型与我们常见的数据类型区别指针的数据类型是在数据类型的基础上加上一个*如下所示
//指针的数据类型
char*——字符型指针类型
short* ——短整型型指针类型
int* ——整型指针类型
long* ——长整型指针类型
long long* ——更长的整型指针类型
float* ——单精度浮点型指针类型
double* ——双精度浮点型指针类型
……只要是数据类型再加上*此时的数据类型就会变成指针的数据类型 对于这颗*的理解我是理解成钥匙孔在介绍和*这两个操作符时我有提到过取地址操作符就相当于是取出门牌号而解引用操作符就是门的钥匙那现在我们从指针的数据类型就可以知道了为什么是*而不是#甚至是其它的符号因为钥匙的形状要和钥匙孔对的上才行。 那现在问题来了这些类型与普通的数据类型有什么区别呢这里我们通过sizeof来测试一下 从测试结果中我们可以看到在32位操作系统下不管是哪种类型的指针此时所占空间大小都是4个字节也就是32个比特位下面我们来看一下在64位操作系统下又是什么情况 可以看到此时的大小为8个字节也就是64个比特位这个数值有没有感觉很熟悉 没错这个和地址在这两个操作系统下的大小是一致的这一点可以直接证明指针就是地址。
那既然不管什么类型的指针所占空间大小都是一样的那是不是说我随意定义一个类型的指针就可以了呢不同类型的指针有什么区别呢
三、指针类型的意义
对于前面定义的整型变量a以及还未确定类型的指针p为了探究不同类型指针的意义我们分别用char类型、short类型、int类型以及long long类型的指针来接收变量a的地址如下所示
//指针类型的意义
int main()
{int a 4;//通过取地址操作符将变量a的地址取出来存放在指针变量中char* p1 a;short* p2 a;int* p3 a;long long* p4 a;return 0;
}此时我们已经完成了指针变量的创建接下来我们分别通过对指针进行整数加减以及通过解引用来完成对变量a存储内容的修改我们来看看不同类型的指针都会有哪些差异
3.1 指针 ‘’/‘-’ 整数
因为指针存储的是地址所以指针加减整数实质上就是地址进行整数的加减为了更加直观的看到其变化我们通过打印格式%p——以地址的形式打印我们现在对这四种指针类型分别进行1和-1的操作测试结果如下所示 从测试结果中我们可以看到 对于char*类型的指针p1来说它加减1的值刚好是1个字节的大小 对于short*类型的指针p2来说它加减1的值刚好是2个字节的大小 对于int*类型的指针p3来说它加减1的值刚好是4个字节的大小 对于long long*类型的指针p4来说它加减1的值刚好是8个字节的大小 大家应该对char、short、int、long long这些数据类型所占空间大小应该还有印象吧没印象也没关系如下图所示 现在大家有什么发现吗 没错不同类型的指针在进行加1和减1操作后指针变化的字节大小与对应的数据类型所占空间大小相同 那如果是2就相当于是1之后再1那指针变化的字节大小应该是对应数据类型所占空间大小的2倍。同理3就是3倍4就是4倍……n就是n倍那具体是不是这样呢我们继续测试 从测试结果中可以看到不管是2/-2也好还是10/-10也好指针变化的大小确实是对应数据类型的整数倍。因此我们可以得到结论 指针 / − 整数后变化的值 指针对应数据类型所占空间大小 ∗ 整数 指针/-整数后变化的值指针对应数据类型所占空间大小*整数 指针/−整数后变化的值指针对应数据类型所占空间大小∗整数
3.2 指针解引用
接下来我们来看一下对于不同类型的指针进行解引用又会是什么结果
//指针解引用
int main()
{int a1 0x11223344;int a2 0x11223344;int a3 0x11223344;int a4 0x11223344;//通过取地址操作符将变量a的地址取出来存放在指针变量中//指针类型 数据类型*char* p1 (char*)a1;short* p2 (short*)a2;int* p3 a3;long long* p4 (long long*)a4;//通过解引用操作符对指针中存放的内容进行修改*p1 0;*p2 0;*p3 0;*p4 0;return 0;
}这里我们通过四个变量来进行解引用为了方便观察我们通过调试内存窗口来观察不同类型的指针解引用的变化 从内存窗口我们可以看到 对于char*类型的指针p1在通过解引用将地址中存储的值改为0时p1改变了1个字节的内容 对于short*类型的指针p2在通过解引用将地址中存储的值改为0时p2改变了2个字节的内容 对于int*类型的指针p3在通过解引用将地址中存储的值改为0时p3改变了4个字节的内容 对于long long*类型的指针p4在通过解引用将地址中存储的值改为0时p4改变了8个字节的内容 可以看到这个改变内容的字节大小与指针对应的数据类型所占空间大小也是相同的也就是说不同类型的指针在进行解引用操作是可以操作的字节大小与对应类型所占空间大小相同。
经过这两次测试的结果对于不同类型指针的意义现在我们可以得到的结论
不同类型的指针在进行/-整数时指针变化的值为对应类型所占空间大小与整数的乘积不同类型的指针在解引用时对值修改可操作的字节大小为对应类型所占空间大小
现在我们已经知道了什么是指针也知道了指针类型的意义现在我们来看一个新的概念——野指针
四、野指针
我看到野指针的这个野时联想到的是野猫、野狗、野猪……现在问题来了它们为什么被称为野猫、野狗和野猪呢 是因为它们和家养的小动物的区别是家养的小动物是有明确的主人喂养的而这些野生的小动物都是流浪在野外的。 对于家养的小动物来说我们只需要通过它们主人的住址就能找到它们但是野生的小动物你即使知道它的活动区域也不一定能找到它因为它们的位置是不可知的。
野指针也是一样下面我们来看一下野指针的定义
4.1 定义
野指针就是指针指向的位置是不可知的随机的、不正确的、没有明确限制的
4.2 野指针的成因
既然这些指针指向的位置是不可知的那它们是怎么出现的呢 对于野指针的出现主要是三个原因
指针未初始化指针越界访问指针指向的空间被释放了
下面我们对这些原因进行一一的说明
4.3 指针未初始化
指针变量实质上也是一个变量只不过它存放的是地址而已既然是变量那我在创建时如果未给变量进行初始化那么就会导致此时的指针变量指向的是一个随机的地址那如果我要对这个随机的地址进行解引用并对地址中的内容进行修改那会出现什么情况呢 此时我们可以看到在VS2019编译器下直接对这个错误进行了报错报错的原因就是未初始化的局部变量p也就是说此时都不需要你去思考如何操作了编译器直接不给你过。 但是在其它的编译器下可能会正常运行但是此时*p对内存的访问其实是非法的。
我们在函数栈帧篇章有介绍过局部变量的创建是在main函数的栈帧中实现的即使是进行函数调用也是得先将调用的函数的函数栈帧创建好了才能进行后续的操作此时的*p 却是在内存中随意找寻的一块地址那就会出现以下的情况 当指针指向main函数的栈帧中的空间时你并不能确定它指向的是哪一块空间也就是说指针p此时可能指向已经被使用的地址那此时对这个空间的值进行修改是不是有可能导致我写的代码不能正常的运行呢当指针指向main函数的栈帧外的空间时你就更不能确定它指向的是哪一块空间了也就是说指针p此时可能指向的是调用main函数的函数栈帧中的一块地址那此时对这个空间的值进行修改是不是有可能因为修改的这个值导致main函数的调用出现错误呢
如果不好理解的话那我们换一个角度来理解 此时的指针p就好比一个旅行者张三他跑到一个陌生的城市旅游此时他是需要给自己找一个住处的。他寻找住处的方式就是通过酒店的房间地址来明确居住的房间。 给指针初始化的过程就是张三在酒店前台登记入住的过程登记好了才能正常在酒店入住 不给指针初始化就好比这个张三随意跑到一个房间居住这个房间可能是酒店的房间也可能是别人的住宅不管是哪种情况这种行为都是违法的。 所以不给指针初始化指针就会进行非法访问。 4.4 指针越界访问
当我们正常的给指针初始化后也可能出现野指针的情况如下所示 在这个代码中对于数组arr来说它的空间内只有3个元素我们通过数组名将数组的首元素地址赋值给变量p后变量p在进行对地址内容修改时修改了5个地址此时系统就报错了报错内容为变量arr周围的栈区被损坏此时就是指针进行越界访问了。
这种情况就好比 还是这个张三他此时开了三间房并且在酒店前台登记了结果他在入住时不仅将开好的三间房中放置了自己的行李他还将自己的行李放在了另外两间房间内 这种情况下对于酒店来说张三对未登记的两间房间进行了越界访问这种行为也是不可取的 所以指针也是不能进行越界访问的 4.5 指针指向的空间被释放
当指针已经初始化了也没有进行越界访问还有可能会出现野指针的情况如下所示 在这个代码中我们定义了一个整型指针类型的函数test并在函数内部创建了一个变量aa的空间内部存放的值为1此时我们将a的地址返回给函数在主函数中整型指针p接收了这个返回值并将地址中的值打印出来了。 可以看到此时的程序是能正常运行的但是系统会报警告警告的内容为返回了局部变量或零时变量的地址a。 通过函数栈帧的角度来分析的话变量a的地址是创建在test函数栈帧内的当调用结束时test的函数栈帧就被销毁了此时指针p指向的是main函数栈帧外的空间地址虽然是一个明确的地址但是此时的指针p是一个野指针如下图所示 此时的指针p并不在函数栈帧内部所以此时的p也是属于非法访问内存空间的。如果此时我在打印前再调用一个函数我们来看一下会是什么结果 从这一次的测试结果中我们可以看到此时不管是变量a也好、变量b也好还是指针p指向的地址也好它们的地址都是同一个所以此时我们想将a的值通过指针p来打印是做不到的因为此时指针p指向的地址在test2中被使用了所以打印出来的值是被使用后的值也就是现在的100
我们还是通过张三的例子来理解 test函数中的变量a就是李四李四现在在一家酒店登记了并在自己登记的房间里睡了一觉在李四离开酒店的时候他将自己休息的房间号告诉了张三并跟张三说我在房间内给你留了一份礼物于是张三就跑过去拿礼物了 在第一个例子中这时的房间还没有人使用所以张三成功的取到了李四留下的礼物 在第二个例子中在张三过去前房间被王五使用过了结果王五在离开的时候将自己的行李落在了酒店等张三到达酒店时拿到的就是王五的行李 以上这两种行为对于酒店来说都是不合法的所以酒店会提出警告 4.6 如何规避野指针
既然野指针会产生这些问题那我们应该怎么做才能规避野指针呢 方法很简单只要将上述问题反着来就好了
给指针进行初始化避免指针越界访问不要返回局部变量或者临时变量的地址当指针指向的地址不再使用时将指针置为空(NULL)在使用指针前检查指针的有效性
对于前面三点我相信大家现在都是能理解的但是对于第4点可能就有朋友有疑问了为什么当地址不再使用时要将指针置为空呢
这是因为一个约定俗成的规则
当指针为空指针时就不会对指针进行访问所以就有了第5点在使用指针前需要检查指针是否为空指针。
现在大家对指针以及野指针应该是比较熟悉了下面我们来看一下指针是如何进行运算的
五、指针运算
对于指针的运算共有三种运算方式
指针±整数指针-指针指针的关系运算
5.1指针±整数
对于不同类型的指针加/减整数实质上就是指针加减对应数据类型所占空间大小与整数的乘积。 如 char*类型的指针进行整数的加减就是指针加减char类型所占空间大小与整数的乘积int*类型的指针进行整数的加减就是指针加减int类型所占空间大小与整数的乘积 即 p N p s i z e o f ( t y p e p ) ∗ N pNpsizeof(type_p)*N pNpsizeof(typep)∗N p − N p − s i z e o f ( t y p e p ) ∗ N p-Np-sizeof(type_p)*N p−Np−sizeof(typep)∗N 5.2 指针-指针
在数组篇章中我们介绍过数组元素在内存中是从低地址到高地址进行连续存放的下面我们来看一段代码
//指针-指针
int main()
{int arr[10] { 0 };int* p1 arr[0];//首元素地址int* p2 arr[9];//最后一个元素地址printf(%d\n, p2 - p1);return 0;
}现在我们将数组首元素地址与最后一个元素的地址分别提取出来并存放在指针p1和指针p2中下面我们看一下p2-p1会得到什么结果 这个结果似乎与数组元素下标之间的差值是一样的。如果真的是这样的话那我们来测试一下下标为2的元素与下标为7的元素的指针的差值是不是5 从测试结果中可以看到确实如此在数组中数组元素地址之间的差值与下标的差值相等
那问题来了这个差值的含义是什么呢
5.2.1 数组下标差值的意义
我们知道数组的下标就代表数组的元素数组下标是从0开始的那我可不可以认为数组的下标就是代表数组元素前面的元素个数呢
比如首元素的下标为0因为它前面的元素个数为0第二个元素的下标为1因为它前面有1个元素第三个元素的下标为2因为它前面有2个元素以此类推第十个元素的下标为9因为它前面有9个元素
根据这个逻辑我们可以很容易得出元素下标之间的差值就是两个元素之间的个数。 比如这里的下标为7的元素与下标为2的元素下标之间的差值就是从下标为2的元素到下标为7的元素之间的个数它们之间总共有下标为2/3/4/5/6的5个元素 同理首元素与最后一个元素的元素下标之间的差值就是从首元素到最后一个元素之间的元素个数它们之间总共有下标为0/1/2/3/4/5/6/7/8的9个元素
我应该有表述清楚这个逻辑吧相信大家也都能够理解了。这个结论有什么用呢下面我们继续来介绍
5.2.2 strlen函数
在介绍数组时我们有介绍过一个内容——字符串。 在介绍字符串时我们提到了一个计算字符串长度的库函数——strlen 下面我们就来通过MSDN来看一下这个库函数究竟是怎么使用的 从资料卡中我们可以看到strlen函数需要传入一个字符指针类型的参数并返回一个size_t的值返回值为字符串中的字符数不包括\0。也就是说这个函数就是在自动帮我们计算字符串中\0前面的字符的个数。
5.2.3 字符串
对字符串在数组篇章中我们主要介绍了以下几点内容
由双引号引起的一个或多个字符叫做字符串双引号中自带一个字符\0\0在字符串中是位于字符串的末尾也就是字符串的最后一个字符\0是字符串的终止符
也就是说如果将字符串放在字符数组中\0就是数组中的最后一个元素\0的下标就代表着它前面的元素个数。如果我能够知道\0的下标那我就能得出这个字符串的字符个数也就是字符串的长度这样我是不是就能自己实现strlen这个函数了呢
现在我们来整理一下我们已知的信息
首元素的下标为0两个下标之间相差1最后一个元素为\0
现在通过这三条信息我们可以尝试着通过寻找\0的下标的方式来实现strlen函数。下面我们来一步一步的分析如何实现strlen这个函数
5.2.4 strlen函数的模拟实现——寻找\0的数组下标
实现一个strlen函数那我们就需要从函数的返回类型、函数的参数以及函数的实现这三步出发下面我们一步一步的进行分析
5.2.4.1 函数的返回类型
为了避免出现冲突我们将模拟实现的strlen函数命名为my_strlen 根据我们的需求我们现在需要返回的是\0的下标也就是一个整型值也就是说我现在定义的函数返回类型应该是一个int型的函数即int my_strlen()
5.2.4.2 函数的参数
函数的参数我们现在需要思考的是我如何能找到\0的下标 通过已知信息我们可以大致想象一下有两种方式——通过下标来寻找\0的下标、通过寻找\0来寻找\0的下标 如果我们通过下标来寻找的话我们是通过给首元素的下标不断的进行1的操作来寻找\0的下标。那现在问题来了我如何知道这个下标是不是\0的下标呢好像这个方式无法实现那我们再来看另一种方式 如果我们通过寻找\0来寻找元素下标的话那我就需要通过判断此时的元素是否是\0如果不是再判断下一个元素直到找到\0为止像这样看的话好像可行性很高那我们现在就需要知道如何找到数组中的每个元素了
对于如何找到数组中的元素这个问题我相信大家心里都是有一个比较明确的方式了——我们可以通过数组元素的地址来找到数组中的元素。
我们知道在内存中数组元素是从低地址到高地址进行连续存放的相邻的两个元素的地址之间的相差的大小刚好为数组元素的类型所占空间的大小。 也就是在字符数组中两个相邻的元素之间的地址之间相差的大小为1 在整型数组中两个相邻的元素之间的地址之间相差的大小为4 也就是我只要对数组元素的地址加上一个元素的数据类型所占空间大小那就能得到下一个元素的地址了。
从指针±整数的结论我们可以得到
指针加上一个元素的数据类型所占空间大小就等价于指针1
那现在首元素我是知道的我们可以通过取地址操作符将首元素的地址取出来再传给函数my_stlen这样我们是不是就可以通过指针1的方式来找到后面的所有元素了呢
在数组中我们也介绍过数组名就是数组首元素的地址所以此时的实参我们需要传入的是数组名那么形参就需要通过指针来进行接收也就是char* name
5.2.4.2函数的实现
现在我们以及确定了函数的返回类型以及函数的参数我们现在只需要理清函数的实现思路就可以了 在探讨函数参数时我们已经有了一个大致的思路
通过元素的地址1寻找到下一个元素的地址再通过判断元素是否为\0如果不是就继续进行地址1的操作直到找到\0为止
从这个思路中我们可以明确此时的函数是需要通过循环语句实现的下面我们开始编写代码
//strlen函数的模拟实现——寻找\0的数组下标
int my_strlen(char* ch)
{int i 0;//数组下标while (*ch ! \0)//判断元素是否为\0{ch;//地址1找到下一个元素i;//下标1找到下一个元素的下标}//当结束循环时说明已经找到了\0此时我们只需要将下标返回给函数就行return i;
}下面我们来测试一下 从测试结果中我们可以看到此时确实模拟实现了strlen函数。这里看似我们是通过找到\0的下标来实现的实质上是因为首元素下标为0最后一个元素的下标减去首元素的下标还是为最后一个元素的下标。因此这种实现方式我们实质上是通过最后一个元素下标减去首元素下标实现的
这时有朋友就会说了既然指针-指针的结果与下标之间的差值相同那我能不能通过\0的指针减去首元素的指针来实现strlen函数呢 当然可以了下面我们来通过指针-指针的方式实现
5.2.5 strlen函数的模拟实现——指针-指针
我们想要通过指针-指针的方式实现的话那我们就需要记录首元素的指针以及\0的指针才行接下来我们就通过my_strlen2来实现strlen函数
//strlen函数的模拟实现——指针-指针
int my_strlen2(char* ch)
{char* last ch;//ch——首元素的指针//last——\0的指针while (*last ! \0)//判断元素是否为\0last;//地址1找到下一个元素地址//当结束循环时说明已经找到了\0此时我们只需要将\0与首元素的指针的差值返回给函数就行return last - ch;
}可以看到相比于通过下标实现此时的代码相对来说就简洁了一点下面我们来测试一下 可以看到通过这种指针-指针的方式我们也很好的模拟实现了strlen函数。
可以看到这两种实现方式都是通过迭代实现的下面我们来拓展一下思维通过函数递归来实现strlen函数
5.2.6 strlen函数的模拟实现——函数递归
此时如果通过递归实现的话我们就需要思考如何进行递归。 我们现在的起点是首元素终点是\0从起点到终点我们需要做的事情就是两个 指针1找到下一个元素地址判断元素是否为\0 也就是说如果我要递归的话那我递归一次就能找到下一个元素如果我递归n次才找到\0那就说明此时的字符串长度为n 我如果能将递归的次数记录下来是不是就可以得到字符串的长度了 有了具体的思路下面我们就开始实现此时我们将函数命名为my_strlen3
//strlen函数的模拟实现——函数递归
int my_strlen3(char* ch)
{if (*ch \0)//当找到\0时就要开始回归了此时的递进次数为0return 0;//当没找到\0时需要先将指针自增再进行递进return 1 my_strlen3(ch);
}接下来咱们来验证一下 可以看到此时我们也很好的实现了strlen函数。
经过这个模拟strlen函数的实战练习我相信大家对指针±整数以及指针-指针的运算方式都已经熟悉了。在开始介绍下一种运算之前我们需要注意
在数组中指针-指针的意义才是两个指针之间的元素个数当任意两个指针进行相减时得到的是两个指针之间的空间个数
为了更好的说明第二点下面我们来看这两个例子 可以看到此时变量a和变量b的指针相减的结果并不是说a到b之间有三个元素大家如果理解了函数栈帧的话那就应该知道此时的结果是代表变量a与变量b之间有3个空间 在这个情况下它们两个的差值为61这是说明此时变量a与变量b的地址之间有61个空间
对于获取两个变量之间的空间个数意义不大所以指针-指针的应用主要是在数组中进行使用
接下来我们来看一下指针的关系运算
5.3 指针的关系运算
既然指针与指针可以进行相减那么指针之间也是能够比较大小的下面我们来看一个例子 可以看到此时指针pa与指针pb之间正常的进行了大小的比较从比较结果中我们得知pa的值大于等于pb的值。
通过这个例子我相信大家很容易就能理解这个运算。这种运算方式可以让我们在进行条件判断时又多了一种新的思路
通过指针来进行条件判断
下面我们来做一道题 编写函数通过指针来完成对数组的初始化 如果这里没有说通过指针来完成的话那我们按照之前的思路就是通过数组下标来访问数组的每一个元素从而实现对数组的初始化代码如下
//编写函数完成对数组的初始化
void init_arr(int arr[], int sz)
{for (int i 0; i sz; i)arr[i] 0;
}现在我们可以通过指针的方式来进行代码如下
//编写函数通过指针来完成对数组的初始化
void init_arr2(int* arr, int sz)
{for (int* p arr sz; p arr;)*(--p) 0;
}在这个代码中我们通过将数组最后一个元素的下一个地址赋值给指针p此时的p的地址肯定是比首元素的地址大的所以此时我们通过先将p进行自减1再来解引用完成初始化。当p的地址自减为首元素地址并完成初始化之后再进行判断时此时条件不成立结束循环
这一题就是一个简单的使用指针的关系运算的例子大家只需要通过这个例子知道指针的这种运算方式并在之后的解题过程中对解题方式有一个新的解题思路那就可以了。
现在咱们对指针的运算已经全部介绍完了下面我们来思考一个问题——在数组中数组名代表的是首元素的地址如果我将首元素地址存放在一个指针中如下所示
int main()
{int arr[5] { 1,2,3,4,5 };int* pa arr[0];return 0;
}此时的指针pa存放的也是首元素地址那这个数组名arr与指针pa之间有什么关系呢下面就让我们来探讨一下数组与指针
六、指针和数组
对于数组名我们在数组的篇章中有介绍过以下的知识点
数组名存放的是数组首元素的地址当使用sizeof(arr_name)来计算数组大小时此时的数组名代表的是整个数组当使用arr_name来取地址或者是传参时此时的数组名代表的是整个数组我们可以使用arr_name[num]来通过下标num访问数组的各个元素
有细心的小伙伴就会发现在数组篇章中我曾给出过自己从指针的角度来看待数组名时对数组名的理解
数组名此时就相当于是存放了首元素地址的一个指针所以我们可以通过数组名来访问元素的地址。
那究竟是不是这样呢下面我们来做一个测试——我们来通过指针下标来访问数组的元素 可以看到此时我们是可以通过指针数组下标来访问数组元素的
之所以能通过指针访问数组元素就是因为数组元素在内存中是从低地址到高地址进行连续存放的相邻两个地址之间的差值刚好是一个数据元素对应的数据类型所占空间大小。在前面进行模拟实现strlen的时候咱们也证实过了。
通过这个例子我们就能得到结论 数组名 [ 下标 ] ∗ ( 指针 下标 ) 数组名[下标]*(指针下标) 数组名[下标]∗(指针下标)
下面我们来进行第二个测试通过sizeof来分别计算数组名与指针 可以看到在sizeof中数组名此时代表的是整个数组但是在sizeof中指针代表的依旧是数组首元素的地址
那如果我们将整个数组的地址存入指针又会如何呢 可以看到此时就算是将整个数组的地址存放进指针通过sizeof计算的指针大小依旧是一个元素所占空间的大小
这是为什么呢如果数组名就是指针的话那我们此时应该是计算的整个数组大小所占空间大小才对呀下面为了解开这个疑惑我们通过图像来进行理解 在反汇编界面我们找到了指针、变量以及数组的位置接下来我们通过这里提供的信息来分析一下 对于指针来说它们指向的只能是一个地址但是对于数组名来说我们通过取地址操作符取出来的是整个数组的地址只不过这个地址是数组的起始地址在sizeof中我们可以看到 s z 1 sz_1 sz1存储的是14h转换成十进制就是20 s z 2 sz_2 sz2和 s z 3 sz_3 sz3存储的都是4也就是一个元素所占空间大小 从前面的知识中我们已经知道的是 sizeof在计算空间大小时计算的是操作对象所占空间的大小指针此时指向的是数组首元素的地址 也就是说数组首元素的地址并不能代表整个数组但是数组名也是数组首元素地址用sizeof计算数组名时却是得到的整个数组所占空间大小这个又怎么解释呢
对于这个问题我们可以理解为用sizeof计算数组名时数组名表示的是数组所占空间的起始地址 指针 p a 2 pa_2 pa2指向的是整个数组的起始地址而我们通过sizeof计算 p a 2 pa_2 pa2时计算的是起始地址所占空间大小 如上图所示sizeof在计算数组名时计算的实质上是数组中5个元素所占空间的大小总和在计算指针时计算的实质上是数组首元素所占空间大小
因此对于指针与数组的关系我们就有了以下结论
数组名[下标]*(指针下标)在代表数组首元素地址时数组名指针指针指向的永远是一个地址而数组既可以表示一个地址也可以表示整个空间的起始地址 计算空间大小时数组名代表的是数组所占空间大小的起始地址是从起始地址开始计算计算的是整个数组所占空间大小计算空间大小时指针指向的是数组所占空间大小的起始地址计算的是起始地址所占空间大小
现在我们已经介绍完了指针与数组之间的关系不知道大家有没有注意在上面的例子中的反汇编界面的截图中对于指针 p a pa pa和 p a 2 pa_2 pa2来说它们自己所在的空间也是有一个地址并且地址并不相同唯一相同的是地址里存放的内容都是数组首元素的地址。
对于变量也好、数组也好它们的地址我们可以存放在指针中那如果我想存放指针的地址又应该怎么处理呢
下面我们就来介绍一下存放指针地址的指针——二级指针
七、二级指针
指针变量实质上也是一个变量既然是变量那如果存放地址也是需要存放在指针中只不过唯一不同的是存放指针变量的指针我们将其称为——二级指针
7.1 二级指针的创建
二级指针与指针一样都是通过type* name的格式进行创建的只不过有区别的是此时的name是指针所以它本身带有一颗*所以二级指针的创建格式为
//二级指针的创建格式
type** name;
//type——指针对应的数据类型
//type*——指针类型这里的*代表此时的变量类型是指针类型
//*name——指针变量名这里的*代表此时的变量是一个指针
//name——变量名在知道创建的格式后我们就来尝试着创建一个二级指针变量
//二级指针的创建
int main()
{int a 10;//将a的地址存放进一级指针中int* pa a;//将一级指针pa的地址存放进二级指针中int** ppa pa;return 0;
}现在我们就创建好了一个二级指针。此时可能就有朋友的思维开始发散了——既然存放一级指针的指针为二级指针那存放二级指针的指针是不是就是三级指针以此类推就能得到四级指针、五级指针……
这个想法非常正确对于三级指针、四级指针以及多级指针的创建就是如二级指针一样如下所示
//二级指针的创建
int main()
{int a 10;//将a的地址存放进一级指针中int* pa a;//将一级指针pa的地址存放进二级指针中int** ppa pa;//将二级指针ppa的地址存放进三级指针中int*** pppa ppa;//将三级指针pppa的地址存放进四级指针中int**** ppppa pppa;//……return 0;
}这时我们都是可以通过解引用操作得到变量a中存放的内容 多级指针与一级指针相比只是多了解引用的次数本质上都是一样的——存放地址的变量。但是我们几乎很少使用到二级指针以上的指针所以对于多级指针大家只需要理解其实质以及知道如何创建就行。
7.2 二级指针的工作原理
既然二级指针存放的是指针的地址那我们又应该怎么使用它呢为了搞清楚这个问题我们首先要了解二级指针的工作原理 二级指针存放的是一级指针的地址所以我们对二级指针进行第一次解引用时找到的是一级指针存放的内容——变量a的地址 对二级指针进行第二次解引用时就相当于对一级指针进行解引用所以找到的是变量a存放的内容 因此我们可以得到结论 ∗ p p a p a *ppapa ∗ppapa ∗ ∗ p p a ∗ p a a **ppa*paa ∗∗ppa∗paa
既然存放指针地址的指针被称为一级指针那么如果有一个数组它的数组元素为指针那这样的数组又是什么呢
八、指针数组
数组的定义是一组相同数据类型元素的集合那如果元素的数据类型为指针类型这样的集合就被称为——指针数组。
指针数组本质上是一个数组只不过此时的数组元素为指针。
那对于这个数组元素为指针的数组我们应该如何创建呢
8.1 指针数组的创建
一个数组的创建格式为
//数组的创建格式
type arr_name[size];
//type——数组元素的数据类型
//type[size]——数组的数据类型
//arr_name——数组名
//size——数组大小现在我们已经知道了数组元素的数据类型为指针类型也就是type*也就是说对于指针数组的创建格式应该是
//指针数组的创建格式
type* arr_name[size];
//type*——数组元素的数据类型
//type*[size]——数组的数据类型
//arr_name——数组名
//size——数组大小如果我需要创建一个数组名为arr数组大小为3的整型指针数组那我就可以根据格式创建这个指针数组
//指针数组的创建
int main()
{int* arr[3];//int*——数组元素类型为整型指针类型//int*[3]——数组类型为大小为3的整型指针类型//arr——数组名//[3]——数组大小/数组元素个数return 0;
}现在我们已经创建好了一个整型指针数组接下来我们就需要对这个数组进行初始化了。
8.2 指针数组的初始化
对于指针数组的初始化我们有三种方式 通过指针变量进行初始化通过取地址操作符进行初始化通过空指针NULL进行初始化 如下所示
//指针数组的初始化
int main()
{int a 1;int b 2;int* pa a;//变量a的指针int* arr[3] { pa,b,NULL };//指针数组初始化//NULL——空指针return 0;
}指针数组在进行初始化时有以下三种情况 如果明确数组的各个元素可以通过指针变量或者取地址的方式进行完全初始化当只知道数组的部分元素并进行不完全初始化时未被初始化的元素会自动初始化为空指针(NULL)当对数组元素不明确时我们可以通过空指针(NULL)进行初始化 下面我们通过监视窗口来看一下这三种情况下的初始化 从监视窗口中我们可以看到 对于完全初始化的指针数组arr1来说数组内的元素都是明确的 对于不完全初始化的arr2和arr3来说不管是通过指针变量、取地址还是空指针对其进行不完全初始化未被初始化的元素会自动初始化为空指针 对于未初始化的arr4来说指针内存放的值为0xcccccccc这样的随机值并且这个随机值的类型为int*也就是说此时的地址为一个随机的地址前面我们提到过当地址为随机值时此时它时野指针 我们现在也知道野指针会引发的问题所以为了避免指针数组内存放的为野指针我们就必须要对指针数组进行初始化。
九、二级指针与指针数组
一维数组的数组名在除了计算数组所占空间大小以及进行取地址操作这两种情况外数组名就等价于数组首元素的指针。 大家现在思考一下如果我们将一维数组的元素替换成指针类型那与这个数组名等价的数组首元素的指针又应该是什么样的呢
为了搞清楚这个问题下面我们来分析一下 指针存放的是数组首元素的地址此时的数组首元素的类型不是指针类型 当数组首元素的类型为指针类型时此时存放首元素地址的指针存放的应该是一个指针的地址 在前面的介绍中我们有提到过存放指针地址的指针我们将其称为二级指针。也就是说指针数组首元素的指针是一个二级指针根据指针与数组的关系我们可以得到
在除了计算指针数组所占内存空间大小以及进行取地址操作这两种情况外指针数组的数组名等价于一个二级指针
有了这个结论后下面我们来探讨一下对于二级指针与指针数组我们应该如何使用
9.1 二级指针±整数
对于一级指针来说指针±整数得到的值是指针±对应数据类型所占空间大小*整数。那二级指针进行加减整数时又会是什么情况呢下面我们就来测试一下 从测试结果中我们可以看到对于不同类型的二级指针来说在进行加1或者减1时变化的大小都是4个字节。既然这样那我们就用char**的指针ppa为例子来继续测试加减整数 可以看到二级指针ppa在进行加减整数时指针变化的值为4的整数倍。
通过这两次的测试结果我们就能得到以下结论
不同类型的二级指针进行加1和减1后指针变化的值都为4同一类型的二级指针进行加减整数后指针变化的值为4的整数倍
此时有朋友可能就会好奇了为什么是4这个值呢我们先来回顾一下指针加减整数的规则 指针 − N 指针 − s i z e o f ( 指针对应数据类型 ) ∗ N 指针-N指针-sizeof(指针对应数据类型)*N 指针−N指针−sizeof(指针对应数据类型)∗N
下面我们来看一下一级指针的创建格式
//一级指针的创建格式
type* p name;
//type——指针对应的数据类型
//*——表示此时的变量为指针类型
//p——指针变量名
//name——被指针指向的变量从这个格式中我们可以看到在type*中type表示的就是指针对应的数据类型前面我们在介绍二级指针时我们是将其中一颗*给了指针变量也就是type* *p p的格式。下面我来给大家介绍一个新的思路我们将二级指针看做一个一级指针然后我们就能得到二级指针的格式
//二级指针的创建格式
(type*)* pp p;
//type*——指针对应的数据类型
//*——表示此时的变量为指针类型
//pp——指针变量名
//p——被指针指向的变量可以看到在这种格式下我们就可以很清楚的看到二级指针对应的数据类型为指针类型。在前面我们也介绍过对于不同的指针类型在32位操作系统下所占内存空间大小都是4个字节在64位操作系统下所占内存空间大小都是8个字节。而前面我们是在32为操作系统下进行测试的这就是为什么对于二级指针进行±整数时变化的值为4的整数倍。
9.2 二级指针-二级指针
对于一级指针来说在数组中指针-指针的值为指针之间的元素个数。二级指针也满足这个运算规则如下所示 我们如果将二级指针看做一级指针的话那这里就相当于是一个指针对应的数据类型为整型指针类型的两个一级指针在进行相减所得的结果也是两个一级指针之间的元素个数。
9.3 二级指针的关系运算
同理对于指针指向的目标为指针类型的一级指针它也是可以进行关系运算的。如下所示 现在大家应该都能理解二级指针和指针数组之间的关系了。与其说是二级指针与指针数组之间的关系倒不如说是一级指针与移位数组之间的关系只不过此时的指针所指向的元素的数据类型为指针类型数组元素的数据类型也为指针类型
现在咱们已经将二级指针与指针数组之间的关系介绍完了。不知道有没有朋友会有疑惑我们这里一直在强调除了用sizeof计算数组所占空间大小和通过取地址操作符取出数组的地址这两种情况外其他的时候指针与数组名是等价的那它们之间可不可以进行相互转换呢下面我们就来探讨一下
9.4 指针与数组名的相互转换
为了探讨指针与数组名能否进行相互转换下面我们先进行第一个测试 使用*(数组名下标)来访问数组元素 可以看到此时对数组名进行解引用操作也是可以像指针一样找到数组各个元素的不管是整型数组也好还是整型指针数组也好都是能够正常访问的 使用指针[下标]来访问数组元素 可以看到此时对指针使用下标引用操作符也是能够正常访问数组的各个元素的不管是整型一级指针还是整型二级指针都是可以正常访问的 进行数组传参时通过指针接收 通过指针来接收数组的传参是没有任何问题的这里我们可以看到指针接收完后还能通过下标引用操作符来找到数组的各个元素 进行指针传参时通过数组接收 通过数组来接收指针的传参也是没有任何问题的这里我们看到数组在接收完后还能通过解引用操作符来访问指针指向的各个对象
经过上面的例子我相信大家对指针与数组名之间的相互转换已经非常熟悉了下面我给大家介绍一些关于指针与数组之间的有趣的变形
9.5 有趣的变形 下标[数组名] 可以看到对于下标引用操作符下标与数组名的位置是可以进行互换的 下标[指针] 既然数组名就是指针那么对于指针来说在使用下标引用操作符时也是同样可以将指针与下标的位置进行互换的 指针[下标] 当我们对指针使用下标引用操作符时就等价于对指针进行解引用再进行解引用后再对其取地址还能得到指针 *数组名 我们可以通过对数组名进行解引用来访问数组的各个元素但是数组名并不能像指针一样进行自增操作 这里是因为对于数组名来说数组名是整个数组在内存中的起始位置此时如果对数组名进行自增就相当与是把整个数组的起始位置进行移动所以此时的数组名表示的也是整个数组并不是数组的首元素这个一定要牢记
9.6 总结
介绍到这里关于指针与数组的关系我们就全部介绍完了下面我们来对这些内容做个总结
有三种情况数组名表示的是整个数组 通过sizeof计算数组所占空间大小使用取地址操作符对数组名进行取地址操作对数组名进行自增自减操作等赋值操作 在其它情况下数组的本质就是指针 数组名 指针 数组名指针 数组名指针 指针 数组名 [ 下标 ] 下标 [ 数组名 ] 指针 [ 下标 ] 下标 [ 指针 ] 指针\数组名[下标]\下标[数组名]\指针[下标]\下标[指针] 指针数组名[下标]下标[数组名]指针[下标]下标[指针] 数组 [ 下标 ] 指针 [ 下标 ] ∗ ( 指针 下标 ) ∗ ( 数组名 下标 ) 下标 [ 数组名 ] 下标 [ 指针 ] 数组[下标]指针[下标]*(指针下标)*(数组名下标)下标[数组名]下标[指针] 数组[下标]指针[下标]∗(指针下标)∗(数组名下标)下标[数组名]下标[指针]我们可以修改指针指向的内容但不能修改数组名指向的内容 即我们可以对指针进行自增/自减等赋值操作但不能对数组名进行自增自减等赋值操作
十、指针数组模拟二维数组
在搞清楚了指针与数组的关系后我们再来探讨一下指针数组。 既然数组的本质就是指针而对指针数组来说数组元素为指针也就是说我可以认为指针数组的元素可以是数组。 在前面介绍二维数组时我们有介绍过一种理解——二维数组可以看做是一维数组的集合。 既然指针数组的元素可以是数组而二维数组可以看做是一维数组的集合那是不是说指针数组与二维数组等价呢
很遗憾的告诉各位指针数组与二维数组并不等价同理二级指针与二维数组也是不等价的。之所以不等价是因为对于指针数组来说数组元素存放的地址可以是不连续的地址但是对于二维数组来说数组元素的地址是连续存放的如下所示 从结果中我们可以看到
对于二维数组的元素地址间的差值刚好是8也就是说二维数组可以看做是三个地址连续存放的一维数组的集合而对于指针数组来说我们可以看到指针数组的元素的地址之间并不相邻
因此指针数组与二维数组并不等价又因为指针数组与二级指针等价的所以二级指针与二维数组并不等价
既然我们从地址连续存放的一维数组的集合的角度来看待二维数组的话那我们就可以通过指针数组来模拟实现二维数组。如下所示 在前面对指针与数组的探讨中我们得到的结论是在对数组元素进行访问时解引用操作符*下标引用操作符[]。因此我们可以通过第二个下标引用操作符来访问指针数组中各元素的数组元素。
指针与数组的关系到这里我们就已经深入探讨完了相信大家对于这些内容都应该有了更加清晰的认识了下面为了更好的介绍之后的内容我们需要先给大家补充一些知识点。
十一、void*指针
在函数中我们有介绍过void表示的是无类型函数中的void表示无返回类型。在指针中同样也有void*这种类型的指针这类指针我们可以称它为无具体类型的指针也可以称为泛型指针。
在前面指针类型的意义中我们提到过指针的类型决定了指针对数据进行一次修改时的可操作空间大小
对于char*的指针来说它修改一次数据可以操作的空间为1个字节。我们让char*的指针接收char类型的对象的地址是比较合适的这样我们在修改内容时可以对char类型的地址中存放的内容进行一个字节一个字节的修改对于int*的指针来说它修改一次数据可以操作的空间为4个字节。我们让int*的指针接收int类型的对象的地址是比较合适的这样我们在修改内容时可以对int类型的地址中存放的内容进行四个字节四个字节的修改对于void*类型的指针来说它可以接收所有类型的对象的地址并不能对其进行解引用以及进行指针的运算
下面我们来通过实例验证一下 从报错中我们可以看到void*类型的指针在接收不管是char类型还是int类型的对象的地址时都是没有问题的但是我们在对其进行解引用、加减整数、以及进行指针-指针时都有出现报错报错的内容总结下来就是——对象具有void类型并且void*的大小是未知的
那对于void*类型的指针来说他能做什么呢下面我们来测试一下 从测试结果中我们可以看到我们可以正常的对指针进行关系运算、打印存储的地址以及赋值的操作
也就是说对于void*指针来说它是无法实现指针的±整数运算、解引用以及指针-指针运算这些运算的但是我们可以对指针变量进行基本的操作。
对于void*指针的使用我们会在后面的内容进行介绍大家不要心急耐心往下继续阅读
十二、关键字const
对于const这个关键字它的中文翻译为常数、恒量恒定的不变的它在C语言中的作用正如它的意思一样将操作对象变成不变的这个关键字我们前面几乎没有遇到过它具体有什么作用呢下面我们就来一起探讨一下
12.1 变量
12.1.1 变量的分类
对于C语言来说变量可分为全局变量和局部变量下面我们来看一下什么是局部变量什么是全局变量
//变量的分类
int a 10;//全局变量
test()
{int d a;//局部变量
}
int main()
{int b 20;//局部变量if (a b){int c a b;//局部变量}return 0;
}
在这个例子中我们分别定义了四个变量根据代码的注释我们可以看到变量a为全局变量变量b和变量c为main函数内部的局部变量变量d为main函数外部test函数内部的局部变量
在C语言中我们将花括号{}称为代码块因为我们所有的代码都是需要再{}内部编写的。对于变量来说在{}外面定义的变量称为全局变量在{}内部定义的变量称为局部变量
12.1.2 变量的生命周期和作用域
变量的生命周期我们可以简单的理解为就是变量的创建与销毁的周期 变量的作用域我们可以简单的理解为就是变量可以使用的区域。
对于全局变量与局部变量来说它们的生命周期与作用域是有区别的
全局变量的生命周期是跟随整个工程的全局变量在创建后除非关闭这个工程否则它会一直存在它的作用域也是作用于整个工程的局部变量的生命周期是跟随创建变量的{}在{}内部创建好局部变量后一旦出了{}局部变量就被销毁了它的作用域也是对应的{}
下面我们通过代码来对全局变量以及局部变量的生命周期和作用域进行说明 在这个代码以及测试结果中我们可以得到以下信息 对于全局变量a来说不管是在test函数内部还是在main函数的内部以及if语句的代码块内部都是可以正常使用的所以此时我们可以说全局变量a此时的使用范围是从它创建后的任何地方都可以进行使用对于局部变量c来说它能在if语句的代码块内部使用也可以在if语句外main函数的代码块内进行使用所以此时我们可以说局部变量c的使用范围是在main函数的代码块内部对于局部变量b和局部变量d来说它们都是可以在自己对应的代码块内部进行使用的所以此时我们可以说局部变量b和局部变量d的使用范围是在它们对应的代码块内部 下面我们继续看下面的代码 可以看到此时代码出现了6处报错报错内容都是未声明的标识符也就是说在报错的这些地方是不存在这些变量的。
现在有朋友可能就有疑问了局部变量出现这种情况我都能理解此时它是因为出了自己的作用域就被销毁了嘛但是你都说了全局变量是跟随整个工程的你这现在不是自己打自己的脸吗
别着急下面我们下面我们继续介绍一个新的关键字——extern——引入外部符号可以引用其它源文件内部定义的全局变量现在我们再来看一下下面的代码 从这次的结果中我们可以看到此时通过关键字extern对全局变量a进行声明后现在再到test函数中使用变量a是没有任何问题的。
但是我们通过对b、c、d进行extern的声明后此时报错了错误内容为无法解析的外部符号这也就是说extern只能对全局变量使用。
因此这个例子再一次证明了局部变量的生命周期与作用域都是自己对应的代码块内部而对全局变量来说我们可以通过关键字extern对变量进行声明所以全局变量的生命周期和作用域是在整个工程内部的。
12.1.3 变量的优先级
现在我们设想一下全局变量和局部变量可不可以同名呢如果可以同名那应该是全局变量优先使用还是局部变量优先使用呢下面针对这两个问题我们来通过代码测试一下 从测试结果中我们可以看到在局部变量a的代码块内部打印的是局部变量a的值而当局部变量被销毁后打印的则是全局变量a的值也就是说当局部变量与全局变量的变量名相同时程序优先执行的是局部变量。因此我们在今后写代码时尽量避免局部变量与全局变量同名的情况。
12.2 const修饰局部变量
const这个关键字它是可以对变量进行修饰的当他修饰变量时会给变量赋予一个常属性使变量不可被改变如下所示 可以看到此时程序的报错内容为表达式必须是可修改的左值也就是说此时被const修饰后的局部变量b是不可像局部变量a一样被修改的
但是为什么我们说它const修饰的局部变量只是拥有了常属性呢这是因为我们此时可以通过指针对其进行修改如下所示 此时我们可以看到程序是正常运行的而且b的值此时也被修改为了20所以被const修饰的局部变量只是拥有了常属性不能直接对其进行更改但是它的本质还是一个变量所以我们可以通过指针来对它的值进行修改。
这种通过地址来修改变量的值的方式是绕过了C语言的语法规则打破了const的规则限制这显然是不合理的那我们应该怎么做才能保证即使拿到了变量的地址也无法对变量进行修改呢
12.3 const修饰指针变量
为了能够在拿到变量地址后也无法修改变量的值我们可以通过const对指针进行修饰。但是应该如何修饰呢下面我们来看一段代码 在这个代码中我们通过将const放在指针变量的不同位置来对const进行修饰可以看到此时的程序报错内容是指针pa和pa2这两个const在*左边修饰的指针而对于const在*右边修饰的指针系统并未报错那是不是代表我们可以通过指针来修改变量的值呢下面我们继续测试 从测试结果中我们可以看到此时变量的值确实通过指针pa被修改了也就是说如果我们想限制指针无法通过解引用修改指向的对象中存储的内容那我们就需要将const放在*左边对指针进行修饰才行。
根据const的语法规则如果我们将const放在*右边时此时const限制了什么内容呢下面我们继续测试 在这个代码中我们想通过指针变量p完成对变量a以及变量b的修改可是我们可以看到在完成对变量a的修改后我们将指针p指向b时此时系统报错了报错内容为此时的变量p是无法被修改的。 那如果此时const放在*的左边能不能对指针指向的对象进行修改呢我们继续测试 可以看到此时的指针p是可以对指向的对象进行修改的。通过上面的测试我们可以得到结论 当const在指针*左边修饰指针变量时限定的是*p即无法对*p的值进行修改但是可以对指针p指向的对象进行修改当const在指针*右边修饰指针变量时限定的是p即无法对指针p指向的对象进行修改但是可以对*p的存储内容进行修改 在前面我们在介绍野指针时有说过我们可以通过下面五点来规避野指针
给指针进行初始化避免指针越界访问不要返回局部变量或者临时变量的地址当指针指向的地址不再使用时将指针置为空(NULL)在使用指针前检查指针的有效性
既然我们需要再使用指针前检查指针的有效性那我们应该怎么做呢这就是我们现在要介绍的一个新的知识点——assert断言
十三、assert断言
在头文件assert.h中定义了一个用于在运行时确保程序符合指定条件如果不符合就报错终止运行的宏——assert()。这个宏常常被称为“断言”。
13.1 assert工作原理
assert()这个宏可以接收一个表达式作为参数。如果表达式为真返回值非零assert()不会产生任何作用程序继续运行。如果表达式为假返回值为零assert()就会报错在标准错误流stderr中写入一条错误信息显示没有通过表达式以及包含这个表达式的文件名和行号。
借助这个宏我们就可以在使用指针前来检查指针的有效性如下所示 可以看到当assert的括号内的条件不满足时此时系统就会报错在报错中会显示文件的路径以及报错的具体位置同时系统也会弹出调试错误的窗口。
13.2 NDEBUG
当我们在确保程序没问题后不需要进行断言时我们可以在头文件语句前定义一个宏NDEBUG。此时在重写编译程序时编译器就会禁用文件中的所有assert()语句。当遇到新问题时我们只需要将这个宏注释掉就能继续启用assert()语句来检测程序的问题了。 可以看到此时虽然指针是空指针但是因为NDEBUG的加入assert()并未启用所以正常打印了hehe如果我们将它注释掉它就又会正常启用assert()如下所示 13.3 assert的优缺点
对于程序猿来说assert()还是非常友好的 它能识别并自动表示文件和出现问题的行号通过与NDEBUG来配合使用就能实现开启或关闭assert()机制 但是因为引入了额外的检查所以在使用assert()时会增加程序的运行时间。
对于断言功能一般我们是在Debug版本中使用这样能够帮助我们来排查程序中存在的问题为了不影响用户使用程序的效率我们在Release版本中禁用assert就可以了。对于VS这样的集成开发环境中在Release版本中编译器会直接帮我们将assert给优化掉。
十四、字符指针变量
char*类型的变量被我们称为字符指针变量我们在使用时有以下几种使用方式
//char*指针变量
int main()
{char a a;char ch[5] abcd;char* pa a;//指针指向字符变量char* pc ch;//指针指向字符数组首元素地址char* p abcd;//指针指向字符串地址return 0;
}对于指向字符变量和指向字符数组这两种用法大家应该都是比较熟悉的现在我们要探讨的就是第三种用法指向字符串的地址这个字符串的地址是什么下面我们来看一下 在监视窗口中我们可以看到此时字符串中存放了5个元素字符指针p指向的是字符串的首字符a大家此时有没有一种熟悉的感觉字符串好像和字符数组有点相似那字符串与字符数组到底是不是一样的呢下面我们就来探讨一下
14.1 字符串和字符数组
为了更直观的看到字符串与字符数组的相关信息我们还是借助监视窗口来观察 此时我们可以看到从内容的存储上来对比的话字符串与字符数组是没有区别的元素都有对应的下标并且下标都是从0开始依次递增。 唯一有区别的就是数组名表示的是数组的起始地址也就是数组首元素的地址而字符串的值就是字符串但是当我们用字符指针变量来指向字符串时指针指向的是字符串的首字符地址。
既然它们的区别不大那我是不是可以通过字符指针对字符串进行像字符数组一样的操作呢 从结果中我们可以看到此时我们正常对字符数组的元素进行了修改并成功进行了输出但是通过字符指针变量对字符串进行修改后并未进行输出为什么会这样呢我们还是通过监视窗口来进一步观察 此时我们可以看到程序在运行到通过指针进行对字符串元素修改时程序进行了报错报错内容为写入访问权限冲突也就是此时是不可以进行写入的。为什么会这样呢
这里我们就需要引入一个新的概念——常量字符串。在介绍常量字符串之前我们先要弄清楚什么是常量。
14.2 常量
常量顾名思义就是不变的量在C语言中常量有四种分类
字面常量const修饰的常变量枚举常量#define定义的标识符常量 const修饰的常变量 在前面我们已经介绍了const修饰的局部变量当局部变量被const修饰后我们经过测试发现并不能对局部变量直接进行修改这就表示此时的局部变量拥有了常量属性即不能被更改的属性 但是我们可以借助指针来对局部变量的值进行修改这就表示此时的局部变量还是一个变量可被修改。所以被const修饰的局部变量我们就将其称为常变量。 字面常量 字面常量是我们现在要重点介绍的内容。所谓的字面常量我们可以简单的理解为我可看到的1/2/3/4……这些数字、a/b/c/d……这些字符、以及由这些字符组成的字符串等这些已经被定义好的值。
对于这些常量来说它们都有一个共同点——值是确定的不可被修改的。如下所示 从程序报错中我们可以看到此时的报错内容都是表达式必须是可修改的左值这就是常量的属性——不可被修改。
14.3 常量字符串
对于一个明确的字符串来说它本身是一个常量当我们将字符指针指向常量字符串时此时的字符串可以通过指针进行访问 但是我们不能通过指针对字符串中的元素进行修改。 常量字符串与字符数组类似字符串中的元素也是有对应的元素下标并且下标是从0开始逐渐递增。当我们通过字符指针指向常量字符串时指针指向的实质上是常量字符串的首元素地址。
为了能够避免出现使用字符指针来修改常量字符串的内容所以我们在定义字符指针时最好是通过const对指针变量进行修饰
//const修饰字符指针变量
int main()
{const char* p abcd;return 0;
}当我们将常量字符串放在数组中时实质上是在函数栈帧上开辟了一块新的空间在空间内存放了对应的字符我们通过指针或者是数组名[下标]对数组元素进行更改时实质上是在对新开辟的这块空间存储的内容进行更改并不是对这些常量字符进行更改 在计算机内存中常量都是有自己的地址的。 我们将常量值存放在数组中时计算机就会通过常量的地址找到对应常量的值并将该值存放在数组中对应的元素地址下所以此时我们是可以修改数组元素存放的值 但是对于常量字符串来说我们将其用字符指针指向时是指向的常量字符串自己本身的地址如果我们通过字符指针对其进行修改就好比我们要强行的实现12这样的等式这显然是不合理的 下面我们再来看一个代码
//常量字符串
int main()
{const char* p1 abcd;const char* p2 aefg;const char* p3 abcd;printf(p1 %p\n, p1);printf(p2 %p\n, p2);printf(p3 %p\n, p3);return 0;
}大家觉得在这个代码中对于这三个字符指针存储的地址是相同的还是不相同的 从测试结果中我们可以看到
指针p1和p3因为都是指向的常量字符串abcd所以它们此时存储的地址是相同的指针p2指向的是另一个常量字符串aefg这个字符串的起始地址与abcd的起始地址肯定是不一样的。在这个例子中这三个指针指向的常量字符串虽然它们首元素存储的值都是字符a但是此时它们就相当于是两个字符数组只是首元素存储的值一样但是数组在内存中申请的空间却不是同一块指针p1和p3指向的是同一个字符数组所以它们指向的地址是同一个地址指针p2指向的是不同的字符数组所以它指向的地址是不同的地址
下面我们再来看一个代码
//常量字符串
int main()
{char ch1[5] abcd;char ch2[5] abcd;char* p1 ch1;char* p2 ch2;printf(p1 %p\n, p1);printf(p2 %p\n, p2);return 0;
}在这个例子中大家觉得此时这两个指针所指向的地址是否相同呢 从测试结果中我们可以看到此时两个指针存储的地址并不相同。这是因为
此时这两个指针指向的是两个字符数组虽然两个字符数组中存储的元素是相同的但是数组在内存上申请的空间地址并不是同一块
14.4 总结
相信大家此时对字符指针以及常量字符串与字符数组的区别已经理解了下面我们来对这一块内容做个总结
字符指针在指向常量字符串时需要使用const进行修饰常量字符串相当于一个不可被修改的字符数组字符串的元素下标是从0开始依次递增我们可以通过下标引用操作符对常量字符串中的元素进行访问但不可对其进行修改同一个常量字符串的地址是相同的不同的常量字符串即使首元素相同首元素的地址也不相同
十五、数组指针
指针类型我们前面介绍的有字符型指针、整型指针、浮点型指针等等我们现在要介绍的是另一种类型的指针——数组型指针。
15.1 数组类型
这时有朋友就会说了啊怎么还有个数组型数组型是什么数据类型别着急我们先回顾一下数组
//数组的创建格式
type arr_name[size];
//type——数组元素数据类型
//type[size]——数组数据类型
//arr_name——数组名
//size——数组大小之前我们一直关注的是数组元素的数据类型、数组名以及数组大小我们一直忽略了一个信息那就是数组数据类型type[size]。我们根据这里的创建格式可以看到所谓的数组数据类型实质上就是数组元素数据类型数组大小。
现在我们知道了什么是数组类型接下来我要给大家介绍一种新的观点来看待数组——数组类型的变量
15.2 数组类型的变量
对于数组来说其实我们可以用变量的角度来看待数组如下所示
//数组类型的变量
type variate_name[size];
//type[size]——数据类型
//variate_name——变量名
//size——数据类型向内存申请的空间个数通过数组类型创建的变量我们将其称之为数组变量简称数组。对于数组变量来说它具有以下的性质 变量在内存中会申请size个连续的空间变量中能存放的数据个数与空间个数相同并且这些数据会从低地址到高地址依次连续存放变量名存放的是这块空间的起始地址也是第一块空间的地址即首元素地址这个连续的空间都有对应的编号这些编号是从0开始依次递增我们将其称之为下标通过变量名下标我们可以找到对应空间的地址当我们向变量中存放数据时这些数据会从0下标开始依次存放进对应的空间中对空间地址进行解引用我们可以找到空间中存放的数据这些数据我们称之为数组元素我们还可以通过下标引用操作符和对应的下标来访问下标对应的数组元素通过下标访问数组元素的形式有 变量名[下标][变量名]下标[下标]变量名下标[变量名]*(变量名下标)*(下标变量名) 当我们对数组元素进行初始化时未被初始化的数组元素会自动初始化为0 15.3 数组变量的创建和初始化
我们以数组变量的观点来对数组创建并初始化的话我们就可以写成以下的形式
//数组变量的创建与初始化
int main()
{int arr[3] { 1,2,3 };//int[3]——数组类型//3——数组类型在内存空间中申请了3个连续的空间//int——申请的每个空间所占内存大小为int类型所占空间大小//arr——数组变量名简称数组名//{1,2,3}——申请的空间中存放的数据return 0;
}现在大家通过这个数组变量的观点对数组应该有了新的认识了。
下面大家可以根据这个观点回答以下两个问题吗
为什么数组名与指针等价为什么解引用操作符与下标引用操作符等价 为什么数组名与指针等价 因为数组名存放的是地址而存放地址的变量我们将其称之为指针。 为什么解引用操作符与下标引用操作符等价 因为下标引用操作符的实质是通过下标找到对应的空间地址再对其进行解引用操作
在了解了数组类型以及数组变量后我们再来看一下数组指针。
15.4 数组指针变量的创建与初始化
指针我们前面说过他的本质就是一个变量。前面我们介绍的指针的创建格式如下
//指针的创建格式
point_type* variate_name;
//point_type*——数据类型为指针类型;
//variate_name——变量名现在我们可以用一个新的角度来看待指针的创建——数据类型指针变量名如下所示
//指针的创建格式
type *point_variate_name;
//type——数据类型;
//*point_variate_name——指针变量名可以看到不管是第一种角度还是第二种角度对于决定是指针类型还是指针变量的关键在于*所以对于指针来说*才是指针定义的关键。
当我们以第二种角度来看待指针变量的创建的话我们此时如果将数据类型变成数组类型时我们就可以得到数据类型为数组类型的指针变量即数组指针变量简称数组指针
//数组指针的创建格式
type(*point_variate_name)[size];
//type[size]——数组类型
//*point_variate_name——指针变量名注对于*和[]这两个操作符来说[]的优先级高于*所以我们需要使用()使*与变量名先结合。 根据指针和指针变量的定义指针就是地址指针变量就是存储指针的变量。我们可以很容易的得到结论——数组指针存放的是指向对象的地址
//数组指针的创建与初始化
int main()
{int a 10;int(*p)[1] a;return 0;
}这里我们通过数组指针p存储了变量a的地址因为此时数组指向的对象存储的数据只有一个所以我们只需要像内存申请一块空间就可以进行存放了。
在这个代码中此时数组指针p的元素下标为0我们可以通过下标引用操作符找到对应空间中存放的信息——变量a的地址 在找到变量a的地址后我们可以通过对其解引用来找到a中存放的数据 因为解引用操作符和下标引用操作符是等价的所以对于数组指针变量我们可以写成以下形式 当我们通过两次解引用操作来访问变量a中存放的数据时此时的数组指针就和二级指针类似当我们通过两次下标引用操作符来访问变量a中存放的数据时此时的数组指针就和二维数组类似
15.5 数组指针与指针数组
前面我们介绍了指针类型的数组——指针数组它与数组指针的创建格式如下所示
//指针数组的创建格式
point_type* arr_name[size];
//point_type*[size]——指针数组类型
//arr_name——数组名
//size——数组大小//数组指针的创建格式
type(*point_variate_name)[size];
//type[size]——数组类型
//*point_variate_name——指针变量名
//size——数组类型向内存申请的连续空间的数量通过对比这两种创建格式我们不难发现它们之间的区别就是指针标志*的结合对象不同
*与数据类型结合就是指针数组*与变量名结合就是数组指针
现在可能有部分朋友感觉更迷糊了难道它们就只是指针标志移动了一下位置其实它们是同一个东西 这个答案是否定的指针数组与数组指针它们是两个东西它们有着本质的区别。下面听我慢慢道来。
15.5.1 数组与指针区别
在前面介绍时我们有说过在某些特定的情况下数组名代表的是数组在内存空间中的起始位置在大部分情况下数组名就是指针
所以我们对数组名和指针的处理也是采取的模糊化的方式将数组名就是看成指针指针看做的就是数组名但是我们要清楚的是数组并不等于指针。
数组与指针的区别主要由以下几点 数组和指针在内存中的空间不相同 数组在内存中申请空间时申请的是一片连续的空间指针指向的空间是内存空间中的一块空间 数组和指针的存储数据个数不同 数组在内存中会根据自己的大小申请对应的空间个数每个空间中都能存放一个数据所以数组能存放与数组大小数量相同的数据指针在内存空间中只申请了一块空间所以指针也只能存储一个数据 数组和指针的工作原理不同 数组是先通过数组名找到数组的空间起始位置再通过对应编号找到要访问的空间最后对空间地址进行解引用找到空间中存储的数据指针则是通过存储的数组首元素地址找到数组的首元素再通过首元素的地址加上各个空间的编号找到各个空间的地址最后通过地址进行解引用找到空间中存储的数据 15.5.2 数组指针与指针数组的区别
在理解了数组与指针的区别后我们再来看一下指针数组与数组指针的区别 指针数组与数组指针的内存空间不同 从反汇编界面我们可以看到
指针数组在申请空间时会根据数组大小来申请对应数量的空间数组指针则只在内存空间中申请了一块空间用来存放地址 指针数组与数组指针的存储数据个数不同 对于指针数组来说它能存储的数据个数与数组的空间大小是一致的就如上图所示对于指针数组arr来说它能存储3个数据对数组指针来说因为它在内存中只申请了一块空间所以它能存储的数据也只有一个 此时我们可以看到数组指针在存放3个元素时系统会报错——初始值设定项值太多。 指针数组与数组指针的工作原理不同 指针数组在读取数据时是先通过数组名找到数组空间的起始位置然后通过空间编号找到对应的空间再对空间中存储的地址找到该地址对应的空间最后通过对该地址进行解引用找到空间中存放的数据数组指针在读取数据时是先通过存储指向空间的起始地址找到对应的空间的起始位置再通过空间编号找到对应的空间最后通过对该空间的地址进行解引用找到空间中存储的数据 现在我们知道了指针数组与数组指针是两个东西了对指针和数组的详细剖析我们知道了指针和数组还是有区别的。前面我们通过两次解引用找到了数组指针指向的对象存储的数据这种工作方式与二级指针是一样的那是不是说明其实数组指针与二级指针是同一个内容的不同形式呢接下来我们就来探讨一下数组指针与二级指针的异同点
15.5.3 数组指针与二级指针
对于同为指针的数组指针和二级指针来说它们有很多相同的地方 内存中申请的空间相同 对于指针来说指针指向的是对象的地址指针在内存中申请空间只会申请一块空间来存储指向对象的地址。 不管是一级指针、二级指针还是数组指针也好只要是指针它们都只会在内存空间中申请一块空间来存放数据 工作原理相同 二级指针是先通过存储的一级指针的地址找到一级指针再对一级指针进行解引用找到一级指针中存储的变量的空间地址然后通过变量的空间地址找到变量最后再对变量的空间地址进行解引用找到变量中存放的数据数组指针是先通过存储的对象在内存空间中的起始地址找到对应空间的起始位置在通过空间编号找到对应的空间地址最后通过对空间地址进行解引用找到地址中存放的数据 二级指针是通过两次解引用来找到对应的数据而数组指针通过空间编号找到对应空间的这个过程就是一次解引用的过程所以两种指针在寻找数据的工作原理上是相同的都是通过两次解引用来找到对象中存储的数据
但是二者又有很多不同的地方 指向的空间不同 二级指针指向的是一块空间数组指针指向的是一块连续的空间 指向的对象不同 二级指针存放的是一级指针的地址指向的是一个一级指针数组指针存放的是一块连续的空间的起始地址能够在内存空间中申请一块连续空间的对象目前我们所学的就是数组所以数组指针指向的其实是一个数组 从监视窗口我们可以看到二级指针存储的对象的数据类型为int*即指针类型所以二级指针指向的是一个指针 数组指针存储的对象的数据类型为int型并且指向的对象有三个元素所以数组指针指向的是整个数组并且这个数组是一个一维数组
综合上面的异同点我们可以得出结论
数组指针和二级指针没有关系它们是两种类型的指针数组指针指向的是一个一维数组
在前面的探讨中我们发现数组指针在访问数据时的方式和二维数组也很相似那数组指针和二维数组又是什么关系呢
15.5.4 数组指针和二维数组
在探讨数组指针和二维数组的关系前我们先来回顾一下对应的知识点 二维数组是同一类型的一维数组的集合二维数组的数组名存放的是二维数组在内存空间中的起始位置同时也是二维数组第一个元素的地址即一个一维数组的地址数组指针是一个数组类型的指针数组指针指向的是一个连续空间的起始地址即一个一维数组的地址 既然二维数组名表示的是一个一维数组的地址而数组指针指向的也是一个一维数组的地址那是不是说明二维数组与数组指针等价呢下面跟着我的思路咱们一步一步的来探索
首先我们通过一维数组的角度来看待二维数组
//用一维数组的角度看二维数组
type arr_name[size][num];
//type[num]——数据类型
//type[size][num]——数组类型
//arr_name——数组名
//num——数据类型在内存空间中申请的空间个数
//size——数组大小此时如果我们通过数组下标来访问二维数组的元素时我们是以arr_name[下标]的形式进行访问如果写成指针的形式则是*(arr_name下标)。
然后当我们访问首元素时此时的下标为0也就是说我们可以写成*arr_name这种形式最后当我们继续通过数组下标对首元素的数组元素进行访问时此时的数组名就是指向首元素的指针也就是说我们可以通过指针来代替数组名即*point_name[下标]
下面我们再来数组指针
//数组指针
type(*point_name)[size];
//type[size]——数据类型
//*point_name——指针名
//size——数据类型在内存空间中申请的空间个数当我们通过数组指针访问指向的数组时我们需要通过数组的下标找到数组元素的地址即point_name[下标]当我们找到数组元素的地址后我们可以对其进行解引用来访问下标对应的数组元素即*point_name[下标]
经过对比可以看到数组指针和二维数组不能说是一模一样吧只能说是没有区别。那是不是这样呢下面我们通过代码来验证一下 从监视窗口中我们可以看到指针存储p存储的内容为数组arr首元素的地址指针p在加1后指向的地址是数组第二个元素的地址。
也就是说数组指针p可以通过数组元素下标来访问二维数组的各个元素此时的指针p是与二维数组的数组名等价的。为了进一步验证这个结论我们来进行以下的测试 通过数组下标访问数组的各个元素 此时我们通过两次解引用不管是使用数组名还是指针名都成功的访问到了数组各每个元素 通过解引用操作访问二维数组的各个元素 通过解引用操作我们也成功的访问到了二维数组的各个元素 通过数组指针接收二维数组 我们在对二维数组进行传参时数组指针很好的接收二维数组并成功通过下标对数组元素进行了访问 4.通过二维数组接收数组指针 我们在对数组指针进行传参时二维数组很好的接收了数组指针并成功通过数组下标对指针指向的数组的数组元素进行了访问
经过咱们对数组指针深入且细致的探讨我相信大家应该已经完全理解了数组指针下面我们就来对数组指针做一个总结
15.5.5 总结
数组指针是一个数组类型的指针指向的是一个一维数组当二维数组的数组名指向首元素时数组指针与二维数组的数组名等价数组名可以与数组指针相互转换数组不等于指针数组指针与指针数组是两个不同的概念数组指针是一个一级指针而指针数组相当于一个二级指针
十六、函数指针变量
C语言学习到现在不知道大家有没有发现一个有趣的事情那就是C语言的命名特别的简单粗暴 对不同类型的数组命名是字符数组、整型数组、浮点型数组、指针数组……这些数组的前半部分说明了数组元素的数据类型对不同类型的指针命名是字符指针、整型指针、浮点型指针、数组指针……这些指针的前半部分就说明了指针指向的对象 根据这个命名特点我们不难得出函数指针变量即函数指针它指向的对象应该是一个函数。那它指向的是函数名还是函数名呢
下面我们就来探讨第一个问题——函数名是什么
16.1 函数名
【函数栈帧的创建和销毁】篇章中有提到过我们在调用函数时函数会先通过 ebp 和 esp 这两个指针在内存空间中创建一块空间供函数使用这块空间我们称之为函数栈帧。 对于变量来说变量所在空间的地址就是变量的地址我们通过变量名将变量的地址取出来后存放进指针此时指针指向的就是变量的所在空间对于数组来说数组的起始点的地址就是数组的地址同时也是数组首元素所在空间的地址 我们通过数组名将数组首元素所在空间的地址直接存放进指针此时指针指向的就是数组的首元素的所在空间我们通过数组名将数组在内存空间中的地址的起始地址取出来后放进指针中此时指针指向的就是数组的起始点 那对于函数来说函数名又代表的是什么呢下面我们就来做个测试 从这次测试结果的报错中我们可以得到以下信息
函数名和函数名都是地址函数名和函数名都不能进行± 整数
既然函数名和函数名都是地址它们的地址又会有什么区别呢下面我们继续测试 从反汇编窗口中我们可以看到我们在调用函数的时候得到的函数地址与函数名存放的地址以及函数名得到的地址是一致的也就是说函数名和函数名代表的都是函数的地址 在进入函数后我们从内存窗口和监视窗口可以得到以下的信息
函数的地址并不是函数的栈顶通过指针-指针我们可以发现函数的地址与函数栈顶之间相距1,095,684个空间
通过以上信息我们可以得到结论
函数名和函数名代表的都是函数的地址但是这个地址不是函数的栈顶地址函数名和函数名不能进行±整数
既然函数名和函数名都代表的是函数的地址那既然是地址就能存入指针中此时指向函数的指针我们将其称之为函数指针变量简称函数指针。
我们应该如何创建一个函数指针呢下面我们来看一下函数指针是如何创建的
16.2 函数指针变量的创建和初始化
我们在创建函数指针时需要声明函数的返回类型、函数参数的类型以及函数指针变量名
//函数指针的创建格式
return_type (*point_name)(parameter_type,……)
//return_type——函数指针指向的函数返回类型
//*point_name——函数指针变量名
//parameter_type——函数参数类型
//return_type (*)(parameter_type,……)——函数指针类型从函数指针的创建格式中有几点需要我们注意一下
函数参数类型的数量与参数的数量要一致参数变量名可以省略当只有返回类型、指针标志以及参数类型时这代表的是函数指针的数据类型
根据函数指针的创建格式我们就可以来创建一下函数指针了
//函数指针的创建
int main()
{//无返回类型函数指针void(*p1)();//字符型函数指针char(*p2)(char);//指针型函数指针int* (*p3)(int*, int);return 0;
}这里我们创建了三种类型的函数指针——无返回类型的函数指针、字符型的函数指针以及指针型的函数指针。从函数指针的创建信息中我们就可以获得以下信息
函数指针p1为无返回类型的指针那p1就不能进行解引用以及指针±整数等操作p1指针指向的函数是一个无返回类型的函数函数没有参数p2指针指向的函数是一个返回类型为char的函数函数的参数也为charp3指针指向的函数是一个返回类型为int*的函数函数的参数有两个分别为int*和int
现在我们函数指针创建好了我们应该如何对其进行初始化呢
对于指针而言初识化的方式有两种明确的指向对象和空指针。函数指针的初始化也是一样当我们有明确的对象时我们可以直接将对象的地址赋值给指针进行初始化没有明确的对象时就可以将指针先置空
//函数指针的创建和初始化
//无返回类型
void test1()
{printf(1 2 %d\n, 1 2);
}
int main()
{//函数指针初始化——置空void(*p)() NULL;//对指针赋值p test1;//函数指针初始化——指向明确对象void(*p1)() test1;void(*p2)() test1;return 0;
}我们在对函数指针置空后在有明确的指向对象时可以在后期进行赋值当有明确的指向对象时因为函数名和函数名代表的都是函数的地址所以我们初始化的方式既可以通过函数名进行初始化也可以通过函数名进行初始化 16.3 函数指针的使用
当指针指向函数时此时我们可以认为指针就代表着函数从而对函数进行直接的调用 对于无返回类型的指针来说我们是不能对指针进行解引用的所以要使用无返回类型的指针调用函数只有这一种方式。但是对于有返回类型的指针我们还可以通过解引用的方式来进行函数调用 相信大家有了前面的知识储备对这一块的内容应该能很容易的理解了。下面我们要介绍一个新的知识点——关键字typedef——数据类型重命名
16.4 关键字typedef
关键字typedef的作用是给数据类型重新命名如下所示 我们现在通过将int重命名为N并用N创建了一个变量a通过输出结果我们可以看到变量可以正常创建并且我们也能通过sizeof计算N所占空间的大小。
看到这里有朋友可能就会说了你这不是多此一举吗用得着将int重命名吗
这里我想说的是如果我们遇到一个比较复杂的数据类型如前面介绍的函数指针的类型int*(*)(int*,int)当我们要通过这个类型创建多个函数指针时每一次都要写一大串的代码是不是就比较麻烦这时我们就可以通过typedef来将此类型重命名为一个很简单的名字如下所示 可以看到我们此时将int*(*)(int*,int)这个类型重命名为了p_int并通过新的类型名创建了两个函数指针这两个函数指针都是可以正常进行函数掉用的。 这里要注意的是对函数指针类型重命名时我们需要将新的名字放在指针标志的括号内才能完成重命名 16.5 有趣的代码
下面我们来看两个有趣的代码
//代码1
(*(void(*)())0)();
这个代码是在干啥呢我们能看到的就是两个解引用操作符、一个void、一个0和一堆的括号这个代码我们应该如何理解呢下面听我慢慢道来
要理解这个代码我们首先要找到我们熟悉的部分比如void、(*)、*()。
这一串我们不熟悉但是void我们熟悉呀它表示的是无类型。
什么东西是无类型的呢我们看一下它的后面是什么——(*)()这个是在干什么
好像还是看不懂但是如果我们将void与它们连起来看我们就得到了void(*)()这个有没有很熟悉的感觉
没错这个就是一个返回类型为void的函数指针类型如果此时我们将这个类型重命名我们就能得到新的表达式
(*(void(*)())0)();
//void(*)()——函数指针类型
typedef void(*point)();
//将void(*)()这个数据类型重命名为point
(*(point)0)();
//point——函数指针类型新得到的这个东西又是啥呢这里我们需要补充一个知识点——数字0为整型。
我们现在在整型的前面放置了一个(数据类型)这是在干嘛 有朋友很快就想到了——强制类型转换。
所以这里其实是将0强制类型转换成了函数指针类型之后我们再对函数指针类型的0进行解引用并在后面加了一个函数调用操作符。
最终我们就能得到结论这里实质上是在进行函数指针的调用
//代码2
void (*signal(int, void(*)(int)))(int);有了前面代码的经验现在我们继续来看这个代码。在这个代码中好多字母啊感觉又看不过来了别着急我们还是先找到熟悉的身影——void(*)(int)这个是一个函数指针类型所以为了更好的观察我们先将他重命名
void (*signal(int, void(*)(int)))(int);
//void(*)(int)——函数指针类型
typedef void(*P)(int);
//将void(*)(int)重命名为P
void(*signal(int, P))(int);
//P——函数指针类型现在我们继续观察有没有什么新的发现
如果我们将signal(int,P)拿掉我们会发现这句代码的最外层还有一个void(*)(int)。此时我们将最外面这个数据类型替换成重命名后的数据类型我们就能得到新的代码P signal(int, P)
这句代码的结构为数据类型 标识符(int, P)此时我们但看标识符部分的内容一个括号加两个数据类型这里很明显是在进行函数传参。signal这个函数它有两个参数——一个是int类型一个是函数指针类型
那它实际上是数据类型 标识符(参数类型参数类型)这个格式是函数声明的格式。
所以这里其实是在进行函数声明声明的是signal这个函数它的数据类型为函数指针类型它有两个参数参数类型分别为整型和函数指针类型。
十七、函数指针数组
在学习了指针和数组后我们会发现数组真的是个好东西只要是相同类型的元素我们都可以将它们放进数组中。比如我们熟知的常见的数据类型的元素——字符型、整型、浮点型…… 字符型的元素我们可以放入字符数组中整型的元素我们可以放进整型数组中浮点型的元素我们可以放进浮点型的数组中 在前面我们还介绍了两种新的类型——指针型、数组型 当我们将指针型的元素放入数组时数组被称为指针数组当我们将数组型的元素放入数组时数组被称为多维数组如二维数组 对于上一篇介绍的函数指针来说它属于指针类型的元素只不过它的具体类型时函数类型的指针就像字符指针、整型指针一样 当我们将字符指针类型的元素放入数组时数组被称为字符指针数组当我们将整型指针类型的元素放入数组时数组被称为整型指针数组同理当我们将函数指针类型的元素放入数组时数组就被称为函数指针数组 既然函数指针数组的数组元素为函数指针类型那我们应该如何创建这么一个数组呢
17.1 函数指针数组的创建
我们在介绍函数指针数组的创建之前我们先来复习一下函数指针的创建
//函数指针的创建格式
return_type(*point_name)((parameter_type, ……);
//return_type——函数的返回类型
//point_name——指针名
//*——指针标志
//parameter_type——函数参数类型在创建函数指针时我们之所以要将指针标志与指针名用括号括起来这是因为我们需要确保指针是与指针名结合起来当指针名与返回类型结合起来时表示的是函数的返回类型为指针类型
//指针函数的创建格式
return_type* point_name((parameter_type, ……);
//return_type*——函数的返回类型为指针型
//point_name——指针名
//*——指针标志
//parameter_type——函数参数类型指针标志与数据类型结合时就是指针型的函数指针标志与变量名结合时就是函数型的指针我这样应该描述清楚了吧。下面我们再来看一下函数指针数组的创建格式
//函数指针数组的创建格式
return_type (*point_arr_name[size])(parameter_type,……)
//return_type——函数返回类型
//*——指针标志
//point_arr_name——指针数组名
//size——数组大小
//parameter_type——参数类型这个创建格式我们可以理解为这是一个函数指针型的数组既然是函数指针那指针标志肯定是要与变量名结合所以需要用()将*和变量名结合起来
下面我们在来回顾一下指针数组的创建格式
//指针数组的创建格式
type* arr_name[size]
//type*——数据类型
//type*[size]——数组类型
//arr_name——数组名
//size——数组大小可以看到我们如果要创建一个指针型的数组的话变量名需要先于[]结合才行所以理论上我们应该写成(*(point_arr_name[size]))的形式但是因为[]的有限级要高于*所以里面的括号可以省略不写于是我们就得到了(*point_arr_name[size])
我们现在知道了函数指针数组的创建格式那它又应该如何初始化呢
17.2 函数指针数组的初始化
经过前面的分析我们确定了函数指针数组它是一个函数型的指针数组所以实际上我们是在给指针数组进行初始化。既然是指针数组那当我们没有明确的指向对象时我们需要使用NULL对指针数组进行初始化 当有明确的指向对象时我们可以直接进行初始化 现在已经知道了函数指针数组的创建和初始化了那函数指针数组我们又应该如何使用呢
17.3 函数指针数组的使用
函数指针数组的使用是函数和数组的一个结合体我们可以通过数组下标找到对应的数组元素因为数组的元素是函数指针类型所以我们在找到元素后可以通过函数调用操作符来调用函数如下所示 看到这里可能会有朋友觉得这个函数指针数组很鸡肋这个东西可以用来干什么呢这样我们就不得不提到函数指针数组的实际运用——转移表
十八、转移表 什么是转移表 这里我们可以简单的理解为函数的中转中我们在调用函数前需要通过一个中转站来进行函数的调用。
这个中转站其实就是函数指针数组。所以转移表的实质就是通过函数指针数组来将复杂的函数调用简单化。如下所示 当然我们在实际使用时调用函数的数量是根据实际情况而定的只要被调用的函数满足以下三个条件即可通过函数指针数组来进行调用
函数具有相同的返回类型函数具有相同的参数数量函数具有相同的参数类型
下面我们通过模拟实现计算器的例子来说明转移表的使用
18.1 计算器的模拟实现 功能需求 我们现在打算实现一个进行两个整型运算的计算器这个计算器具有、-、*、/、、|、^的功能。 函数封装 有了需求现在我们就可以对这些功能通过函数进行封装功能比较简单所以我们直接展示封装代码
//加法
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 Bit_And(int x, int y)
{return x y;
}
//按位或
int Bit_Or(int x, int y)
{return x | y;
}
//按位异或
int Bit_Xor(int x, int y)
{return x ^ y;
}用户界面 做好了计算器的核心内容下面我们就要进行面向用户的程序编写了首先肯定是用户界面用户需要知道他能在这个程序里做什么内容如下所示
//用户界面
void menu()
{printf( 欢迎使用简易版计算器 \n);printf(##########################\n);printf(#### 0.退出计算器 ####\n);printf(#### 1.加 法 运 算 ####\n);printf(#### 2.减 法 运 算 ####\n);printf(#### 3.乘 法 运 算 ####\n);printf(#### 4.除 法 运 算 ####\n);printf(#### 5.按位与运算 ####\n);printf(#### 6.按位或运算 ####\n);printf(#### 7.按位异或运算 ####\n);printf(##########################\n);
}用户功能编写 对于用户来说一个计算器只需要能够输入数据和输出数据以及能够重复进行运算就可以了因此我们可以通过循环语句和输入输出来完成
//用户功能编写
int main()
{int input 0;int x 0;int y 0;int ret 0;int (*p[])(int, int) { NULL,Add,Sub,Mul,Div,Bit_And,Bit_Or,Bit_Xor };do{menu();printf(请输入您想进行的运算序号:);scanf(%d, input);if (0 input){printf(正在退出请稍后\n);Sleep(1000);//停留1秒system(cls);//清空屏幕}else if (1 input input 7){printf(请输入两个整数:);scanf(%d%d, x, y);ret p[input](x, y);printf(%d\n, ret);}else{printf(功能暂未开发请重新输入\n);}} while (input);return 0;
}在定义函数指针数组时我们通过给首元素赋值空指针达到一个占位的效果这样我们在调用函数时就能够根据输入的数值来选择对应的函数进行调用了。 效果演示 运算功能演示 可以看到我们很好的对各个函数进行了调用
报错功能演示 当我们输入0-7以外的数字时系统会进行报错并让用户重新进行选择
退出功能演示 在输入0后系统会先进行提示 在等待1s后系统会先清空窗口的内容然后再退出程序
现在咱们简易的计算器程序的编写就完成了可以看到与以往的对函数调用相比我们通过函数指针数组进行中转调用函数时只需要一句代码通过输入值来确定需要调用的函数这样就提高了代码的编写效率
结语
【C语言必学知识点五】指针篇章的内容到这里咱们就全部介绍完了这个篇章从23.12.06开始编写耗时9天的时间终于完成了全部内容的介绍 4w的总字数感觉都快写一本书了。这篇文章基本上是深度剖析了指针与数组的关系我自己也在编写的过程中对这些知识点有了新的认知和理解同时也希望这篇文章对大家在学习和复习指针的相关知识点时能够提供一点帮助和一些新的理解。
大家可以通过点赞、评论、关注、转发、收藏等方式来支持一下博主最后感谢各位的翻阅咱们下一篇文章再见