公司动态

指针的核心知识(下)

📅 2026/8/28 7:47:30
指针的核心知识(下)
C 指针类型与运算从内存访问步长到传址调用和二级指针指针下C 指针类型与运算从内存访问步长到传址调用和二级指针1。 阅读前问题卡C 指针类型、运算与间接访问1.1 阅读前先看这几个问题1.2 读完后完成这 3 道高频问题1.3 自检清单2。 前言2.1 指针类型决定访问单位2.2 连续内存中的定位与距离2.3 传递副本、传递地址与多级间接访问3。 指针类型的意义3.1 指针解引用3.2 指针加减整数3.3 void* 指针4。 指针运算4.1 指针加减整数4.1.1 指针访问一维数组4.1.2 模拟实现 strlen14.2 指针相减4.2.1 为什么会有指针相减4.2.2 模拟实现 strlen24.3 指针的关系运算5。 传值调用和传址调用6。 二级指针1. 阅读前问题卡C 指针类型、运算与间接访问建议先读第 3 节先抓住“指针类型决定一次访问的字节数和一步移动的距离”这条主线。1.1 阅读前先看这几个问题指针变量的大小与类型无关时char*、int*等类型为什么仍会影响解引用和地址移动void*可以接收哪些地址又为什么不能直接解引用或进行加减整数运算指针加减整数、指针相减和指针关系运算分别表达什么含义各自有哪些使用前提或越界风险文中的两种my_strlen实现分别怎样移动指针、识别字符串结尾并计算长度Swap1为什么不能交换main中的变量而Swap2为什么可以对二级指针ppa执行*ppa和**ppa时分别访问了哪一层对象1.2 读完后完成这 3 道高频问题1.2.1 高频问题 1请说明 C 语言中指针类型的意义它如何影响解引用和指针加减整数void*又有哪些能力与限制1.2.2 高频问题 2指针的加减整数、指针相减和关系运算如何用于遍历数组与计算字符串长度使用这些运算时需要满足哪些前提并注意什么边界1.2.3 高频问题 3请说明传值调用与传址调用的区别为什么Swap1交换失败而Swap2能修改主调函数中的变量并解释二级指针的两层解引用关系。1.3 自检清单我能用一句话说明指针类型为什么会影响内存访问。我能解释char*、int*和void*在解引用与运算能力上的差异。我能说明三类指针运算的含义、前提和越界风险。我能复述两种my_strlen实现如何得到字符串长度。我能根据形参与实参的地址关系解释Swap1和Swap2的不同结果。我能从ppa依次说明*ppa、**ppa对应的变量和修改结果。2. 前言2.1 指针类型决定访问单位一排储物格共用同一套位置编号。有人一次只打开一格有人一次处理连续四格两个人都从同一个编号出发但一次能处理的范围和向前走一步的距离不同。对应到文中的内存模型位置编号是地址处理工具的单位是指针类型打开储物格是解引用向前走一步是指针加一。在文中的示例里char*解引用访问 1 个字节、加一移动 1 个字节int*解引用访问 4 个字节、加一移动 4 个字节。void*只保存位置却没有给出处理单位因此可以接收不同类型的地址但不能直接解引用或进行加减整数运算。2.2 连续内存中的定位与距离一排座位按顺序紧挨着摆放。知道第一把椅子的位置后可以按固定间隔找到后面的任意一把知道起点和终点还可以数出两者之间隔了多少个座位。走过最后一把椅子后再继续使用那个位置就离开了这排座位的范围。对应到数组连续座位是连续存放的元素第一把椅子是首元素地址固定间隔由元素类型决定。p i定位下标为i的元素两个指向同一块内存空间的指针相减得到元素间隔关系运算可以判断当前位置是否到达末尾。字符串示例沿相同思路前进到\0一种实现逐字符计数另一种保存起点并用终点减起点得到长度。2.3 传递副本、传递地址与多级间接访问把两个数字抄在便签上交给别人对方便只能修改便签原来的数字不会变化把两个储物格的位置交给对方对方就能找到原来的储物格并替换其中的内容。对应到函数调用便签是形参接收的实参值副本所以Swap1只交换x和y储物格位置是a和b的地址所以Swap2能通过解引用修改主调函数中的变量。是否需要改变原变量决定了这里应传值还是传址。另一个递进问题是需要改变的对象本身也可能是一张位置记录。把这张记录放进另一个有编号的格子就要先找到记录再沿记录找到最终内容。对应到二级指针ppa保存pa的地址*ppa访问并可修改pa**ppa再通过pa访问并可修改a。3. 指针类型的意义指针变量的大小和类型无关只要是指针变量在同一个平台下大小都是一样的为什么还要有各种各样的指针类型呢其实指针类型是有特殊意义的我们接下来继续学习。3.1 指针解引用对比下面 2 段代码主要在调试时观察内存的变化。// 代码 1#includestdio.hintmain(){intn0x11223344;int*pin;*pi0;return0;}// 代码 2#includestdio.hintmain(){intn0x11223344;char*pc(char*)n;*pc0;return0;}调试我们可以看到代码 1 会将n的 4 个字节全部改为 0但是代码 2 只是将n的第一个字节改为 0。结论指针的类型决定了对指针解引用的时候有多大的权限一次能操作几个字节。比如char*的指针解引用就只能访问一个字节而int*的指针解引用就能访问四个字节。3.2 指针加减整数先看一段代码调试观察地址的变化。#includestdio.hintmain(){intn10;char*pc(char*)n;int*pin;printf(%p\n,n);printf(%p\n,pc);printf(%p\n,pc1);printf(%p\n,pi);printf(%p\n,pi1);return0;}代码运行的结果如下我们可以看出char*类型的指针变量1跳过 1 个字节int*类型的指针变量1跳过了 4 个字节。这就是指针变量的类型差异带来的变化。指针1其实跳过 1 个指针指向的元素所以指针n就是跳过n个指针指向的元素。指针可以n那也可以-n。结论指针的类型决定了指针向前或者向后走一步走多大距离。指针加减整数让指针可以在内存中向前或者向后移动这就为使用指针访问内存打好了基础。3.3 void* 指针在指针类型中有一种特殊的类型是void*类型可以理解为无具体类型的指针或者叫泛型指针这种类型的指针可以用来接收任意类型地址。但是也有局限性void*类型的指针不能直接进行指针的加减整数和解引用运算。举例#includestdio.hintmain(){inta10;int*paa;char*pca;return0;}在上面的代码中将一个int类型变量的地址赋值给一个char*类型的指针变量。编译器给出了一个警告如下图是因为类型不兼容。而使用void*类型就不会有这样的问题。使用void*类型的指针接收地址#includestdio.hintmain(){inta10;void*paa;void*pca;*pa10;*pc0;return0;}VS 编译代码的结果这里我们可以看到void*类型的指针可以接收不同类型的地址但是无法直接进行指针运算。那么void*类型的指针到底有什么用呢一般void*类型的指针使用在函数参数的部分用来接收不同类型数据的地址这样的设计可以实现泛型编程的效果使得一个函数可以处理多种类型的数据在后期的课程中我们会再次讲解。4. 指针运算指针的基本运算有三种分别是指针加减整数指针相减指针的关系运算4.1 指针加减整数4.1.1 指针访问一维数组因为数组在内存中是连续存放的只要知道第一个元素的地址顺藤摸瓜就能找到后面的所有元素。intarr[10]{1,2,3,4,5,6,7,8,9,10};#includestdio.hintmain(){intarr[10]{1,2,3,4,5,6,7,8,9,10};int*parr[0];// 获得第一个元素的地址inti0;intszsizeof(arr)/sizeof(arr[0]);for(i0;isz;i){printf(%d ,*(pi));// p i 是下标为 i 的元素的地址}return0;}上面的代码是i作为偏移量在不断变化使得p i这个地址在变化这个代码也可以换一种写法#includestdio.hintmain(){intarr[10]{1,2,3,4,5,6,7,8,9,10};int*parr[0];// 获得第一个元素的地址inti0;intszsizeof(arr)/sizeof(arr[0]);for(i0;isz;i){printf(%d ,*p);p;// p p 1}return0;}这种写法中如果循环次数控制有问题使得p不断地往后偏移最后超出数组本身的内存区域这时的指针就越界了指针越界后继续使用也会非法访问内存。4.1.2 模拟实现 strlen1#includestdio.h// 计数器的方法size_tmy_strlen(char*str){size_tcount0;while(*str!\0){count;str;}returncount;}intmain(){printf(%zu\n,my_strlen(abcdef));// 这样传参时是把字符串首字符的地址传给 my_strlenreturn0;}4.2 指针相减4.2.1 为什么会有指针相减日期 1 天数 日期 2日期 2 天数 日期 1日期 2 - 日期 1 天数和日期运算的道理一样指针也可以进行类似的运算指针 1 整数n 指针 2向后跳过n个元素后的地址指针 2 - 整数n 指针 1向前跳过n个元素后的地址指针 2 - 指针 1 整数n指针 1 - 指针 2 -n指针相减结果的绝对值是两个指针之间元素的个数。两个指针相减运算的前提是两个指针指向同一块内存空间否则计算没有意义。#includestdio.hintmain(){intarr[10]{1,2,3,4,5,6,7,8,9,10};printf(%d\n,arr[9]-arr[0]);return0;}4.2.2 模拟实现 strlen2// 指针 - 指针#includestdio.hsize_tmy_strlen(char*str){char*endstr;while(*end!\0)// 向后寻找 \0end;returnend-str;}intmain(){printf(%zu\n,my_strlen(abcdef));return0;}4.3 指针的关系运算地址的本质还是数值数值是可以比较大小的所以也就满足关系运算。这次我们重新改造一下前面写过的指针访问一维数组的代码因为数组元素的地址随着下标的增长在不断变大所以arr[0] arr[sz]我们就可以使用指针的关系运算来换一种方式实现。// 指针的关系运算#includestdio.hintmain(){intarr[10]{1,2,3,4,5,6,7,8,9,10};int*parr[0];intszsizeof(arr)/sizeof(arr[0]);while(parr[sz])// 指针的大小比较{printf(%d ,*p);p;}return0;}5. 传值调用和传址调用学习指针的目的是使用指针解决问题那什么问题非指针不可呢例如写一个函数交换两个整型变量的值。一番思考后我们可能写出这样的代码#includestdio.hvoidSwap1(intx,inty){inttmpx;xy;ytmp;}intmain(){inta0;intb0;scanf(%d %d,a,b);printf(交换前a%d b%d\n,a,b);Swap1(a,b);printf(交换后a%d b%d\n,a,b);return0;}当我们运行代码结果如下我们发现其实没产生交换的效果这是为什么呢调试一下试试呢我们发现在main函数内部创建了a和ba的地址是0x00cffdd0b的地址是0x00cffdc4。在调用Swap1函数时将a和b传递给了Swap1函数在Swap1函数内部创建了形参x和y接收a和b的值但是x的地址是0x00cffcecy的地址是0x00cffcf0x和y确实接收到了a和b的值不过x的地址和a的地址不一样y的地址和b的地址不一样相当于x和y是独立的空间那么在Swap1函数内部交换x和y的值自然不会影响a和b当Swap1函数调用结束后回到main函数a和b没有交换Swap1函数在使用的时候是把变量本身直接传递给了函数这种调用函数的方式叫传值调用。我们在学习函数时编写的函数基本都是传值调用。结论传值调用时实参传递给形参的时候形参会单独创建一份临时空间来接收实参对形参的修改不影响实参。所以Swap1是失败的。那怎么办呢我们现在要解决的就是当调用Swap函数的时候Swap函数内部操作的就是main函数中的a和b直接将a和b的值交换。那么就可以使用指针了在main函数中将a和b的地址传递给Swap函数Swap函数里边通过地址间接操作main函数中的a和b并达到交换的效果。#includestdio.hvoidSwap2(int*p1,int*p2){inttmp0;tmp*p1;// tmp a;*p1*p2;// a b;*p2tmp;// b tmp}intmain(){inta0;intb0;scanf(%d %d,a,b);printf(交换前a%d b%d\n,a,b);Swap2(a,b);printf(交换后a%d b%d\n,a,b);return0;}首先看输出结果我们可以看到实现成Swap2的方式顺利完成了任务。这里调用Swap2函数的时候是将变量的地址传递给了函数这种函数调用方式叫传址调用。传址调用可以让函数和主调函数之间建立真正的联系在函数内部可以修改主调函数中的变量所以未来函数中只是需要主调函数中的变量值来实现计算就可以采用传值调用。如果函数内部要修改主调函数中变量的值就需要传址调用。6. 二级指针指针变量也是变量是变量就有地址那指针变量的地址存放在哪里这就是二级指针。#includestdio.hintmain(){inta10;int*paa;int**ppapa;// ppa 就是二级指针变量return0;}对于二级指针的运算有*ppa通过对ppa中的地址进行解引用这样找到的是pa*ppa其实访问的就是pa。intb20;*ppab;// 等价于 pa b;**ppa先通过*ppa找到pa然后对pa进行解引用操作*pa那找到的是a。**ppa30;// 等价于 *pa 30;// 等价于 a 30;二级指针变量取出地址要存储起来也需要一个变量这个时候就有了三级指针当然理论上四级指针也是存在的依次类推。7.回答问题卡问题问题1指针变量大小相同的情况下指针类型为什么仍然重要答指针类型决定了对指针解引用的时候有多大的权限一次能操作几个字节问题2char*与int*解引用时一次可操作的字节数有什么差别答char*的指针解引用就只能访问一个字节而int*的指针解引用就能访问四个字节。问题3指针加减整数时地址变化量与指针所指元素的类型是什么关系答指针的类型决定了指针向前或者向后走一步有多大距离问题4void*可以接收哪些地址又为什么不能直接解引用或进行加减整数运算答可以接受任意类型地址。指针类型决定一次读取多少字节void*不带任何类型标签编译器不知道该读几个字节、该怎样解析二进制数据所以*p直接编译报错。问题5指针相减的结果表示什么两个指针相减需要满足什么前提答指针 - 指针结果的绝对值是两个指针之间元素的个数。两个指针相减运算的前提是两个指针指向同一块内存空间否则计算没有意义。问题6两个my_strlen实现分别怎样找到字符串结尾并计算长度答通过数组首尾相减 int sz sizeof(arr) / sizeof(arr[0]);问题7Swap1为什么不能交换实参而Swap2为什么可以二级指针又比一级指针多提供了哪一层间接访问swap1传值调用时实参传递给形参的时候形参会单独创建一份临时空间来接收实参对形参的修改不影响实参。所以Swap1是失败的。swap2传址调用可以让函数和主调函数之间建立真正的联系在函数内部可以修改主调函数中的变量。多了一层pa的间接访问1.2.1 高频问题 1请说明 C 语言中指针类型的意义它如何影响解引用和指针加减整数void*又有哪些能力与限制指针类型的意义是可以表示其代表的变量的类型。影响解引用和指针加减整数所能控制的字节数解引用时指针类型决定按什么对象类型解释该地址以及一次访问多大范围;加减整数时移动的是若干个所指类型的元素而不是直接加减若干字节。void*先转换成合适的具体对象指针类型才能按该类型解引用或标准指针运算这样的设计可以实现泛型编程的效果使得一个函数可以处理多种类型的数据void*类型的指针不能直接进行指针的加减整数和解引用运算。**1.2.2 高频问题 2**指针的加减整数、指针相减和关系运算如何用于遍历数组与计算字符串长度使用这些运算时需要满足哪些前提并注意什么边界指针加减整数可以在数组中定位元素。p i指向后面第i个元素*(p i)可以访问该元素。单独执行p会让指针移动到下一个元素。遍历时还可以比较当前指针和数组尾后地址例如在p arr sz时继续循环。指针相减得到两个位置之间的元素数量。求字符串长度时先保存字符串起始地址start再让另一个指针逐个字符向后移动直到遇到\0。最后用终止位置指针减去start就能得到\0前的有效字符数。这些运算有明确边界。指针加减后的结果只能落在同一数组内或数组尾后一位不能移到首元素之前。指针相减和用于判断先后位置的关系运算也要针对同一数组范围内的指针。尾后指针可以保存、比较和参与合法的相减但不能解引用也不能继续向数组外移动。待扫描的字符数组必须包含\0。如果没有扫描会越过数组并产生未定义行为不保证一定表现为死循环或程序崩溃。数组名不能自增应先用int *p arr这样的可修改指针变量来遍历。指针相减默认得到元素数量不要把它当成字节数。在满足同一对象有效范围等前提时可以先把两端转换为char*再相减以得到字节偏移。这里转换的是指针类型不是把指针强制转换成整数。1.2.3 高频问题 3请说明传值调用与传址调用的区别为什么Swap1交换失败而Swap2能修改主调函数中的变量并解释二级指针的两层解引用关系。传值是把变量的值传给别的变量传址是把地址传过去。传值调用时实参传递给形参的时候形参会单独创建一份临时空间来接收实参对形参的修改不影响实参。所以Swap1是失败的。Swap2使用指针了在main函数中将a和b的地址传递给Swap函数Swap函数里边通过地址间接操作main函数中的a和b并达到交换的效果。二级指针**pp的两次解引用第一次*pp访问二级指针所指向的一级指针可以读取或修改访问最终的数据这个一级指针保存的地址第二次**pp对该一级指针再解引用访问到最终存放的数据实现从二级指针到原始变量的两层间接访问。