ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

C语言——指针(上),系统梳理指针知识点:指针操作符,指针类型,指针运算,const 修饰,野指针,assert断言,传址调用

C语言——指针(上),系统梳理指针知识点:指针操作符,指针类型,指针运算,const 修饰,野指针,assert断言,传址调用 本文梳理C语言有关指针的全套知识点从指针的底层内存和编码入手详细梳理取地址操作符解引用操作符*指针变量指针运算的基本概念及用法讲解野指针出现的可能原因和规避方法并给出 assert断言排查问题的方法对比传值调用和传址调用弄懂什么情况下需要给函数传址。一、内存和地址1. 内存假设有⼀栋宿舍楼把你放在楼⾥楼上有100个房间但是房间没有编号你的⼀个朋友来找你玩如果想找到你就得挨个房⼦去找这样效率很低但是我们如果根据楼层和楼层的房间的情况给每个房间编上号如······ 二楼101102103··· 一楼201202203···有了房间号如果你的朋友得到房间号就可以快速的找房间找到你。如果把上面的例子对照到计算机中又是怎么样呢?我们知道计算机上CPU (中央处理器)在处理数据的时候需要的数据是在内存中读取的处理后的 数据也会放回内存中那我们买电脑的时候电脑上内存是8GB/16GB/32GB 等那这些内存空间如 何高效的管理呢?其实也是把内存划分为一个个的内存单元每个内存单元的大小取1个字节。计算机中常见的单位:一个比特位可以存储一个2进制的位1或者0其中每个内存单元相当于一个学生宿舍一 个字节空间里面能放8个比特位就好比同学们住 的八人间每个人是一个比特位。每个内存单元也都有一个编号(这个编号就相当 于宿舍房间的门牌号),有了这个内存单元的编 号CPU就可以快速找到一个内存空间。生活中我们把门牌号也叫地址在计算机中我们 把内存单元的编号也称为地址。C语言中给地址起 了新的名字叫指针所以我们可以理解为内存单元的编号地址指针2. 如何理解编址首先计算机内是有很多的硬件单元而硬件单元是要互相协同工作的。所谓的协同至少相互之间要能够进行数据传递。但是硬件与硬件之间是互相独立的那么如何通信呢?答案很简单用线连起来。而CPU和内存之间也是有大量的数据交互的所 以两者必须也用线连起来。不过我们今天关心一组线叫做地址总线。CPU访问内存中的某个字节空间必须知道这个 字节空间在内存的什么位置而因为内存中字节很多所以需要给内存进行编址。(就如同宿舍很多需要给宿舍编号一样)32位机器有32根地址总线每根线只有两态表示0,1那么 一根线就能表示2种含义2根线就能表示4种含义依次类推。32根地址线就能表示2^32种含 义每一种含义都代表一个地址。地址信息被下达给内存在内存上就可以找到该地址对应的数据将数据在通过数据总线传入 CPU内寄存器。我们可以简单理解正如钢琴、吉他上面没有写上“剁、来、咪、发、唆、拉、西”这样的信息但演奏者照样能够准 确找到每一个琴弦的每一个位置这是为何?因为制造商已经在乐器硬件层面上设计好了并且 所有的演奏者都知道。本质是一种约定出来的共识!二、指针变量和地址1. 取地址操作符()理解了内存和地址的关系我们再回到C语言在C语言中创建变量其实就是向内存申请空间比如比如上述的代码就是创建了整型变量a, 内存中 申请4个字节用于存放整数10,其中每个字节都 有地址上图中4个字节的地址分别是0x000000501153FC54 0x000000501153FC55 0x000000501153FC56 0x000000501153FC57那我们如何能得到a的地址呢?这里就得学习一个操作符()-取地址操作符#includestdio.hintmain(){inta10;a;//取出a的地址printf(%p\n,a);return0;}a取出的是a所占4个字节中地址较小的字节的地址。虽然整型变量占用4个字节我们只要知道了第一个字节地址顺藤摸瓜访问到4个字节的数据也是可行的。2. 指针变量和解引用操作符(*)1)指针变量那我们通过取地址操作符()拿到的地址是一个数值比如0x000000501153FC54 , 这个数值有时候也是需 要存储起来方便后期再使用的那我们把这样的地址值存放在哪里呢?答案是 指针变量中。比如#includestdio.hintmain(){inta10;int*paa;//取出a的地址并存储到指针变量pa中return0;}在这里a和pa是同一个意思代表a的地址。指针变量也是一种变量这种变量就是用来存放地址的存放在指针变量中的值都会理解为地址。2)如何拆解指针类型我们看到pa的类型是 int* , 我们该如何理解指针的类型呢?int a 10; int* pa a;这里pa 左边写的是int**是在说明pa是指针变量而前面的 int 是在说明pa指向的是整型(int) 类型的对象。3)解引用操作符我们将地址保存起来未来是要使用的那怎么使用呢?在现实生活中我们使用地址要找到一个房间在房间里可以拿去或者存放物品。C语言中其实也是一样的我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针) 指向的对象这里必须学习一个操作符叫解引用操作符(*)。#includestdio.hintmain(){inta100;int*paa;*pa0;return0;}上面代码中第7行就使用了解引用操作符*pa 的意思就是通过pa中存放的地址找到指向的空间*pa其实就是a变量了所以*pa0, 这个操作符是把a改成了0.其实这里是把a的修改交给了pa 来操作这里如果目的就是把a改成0的话写成 a0;不就完了为啥非要使用指针呢? 这样a的修改就多了一种的途径写代码就会更加灵活3. 指针变量的大小前面的内容我们了解到32位机器假设有32根地址总线每根地址线出来的电信号转换成数字信号后 是1或者0,那我们把32根地址线产生的2进制序列当做一个地址那么一个地址就是32个bit 位需要4 个字节才能存储。如果指针变量是用来存放地址的那么指针变量的大小就得是4个字节的空间才可以。同理64位机器假设有64根地址线 一个地址就是64个二进制位组成的二进制序列存储起来就需要 8个字节的空间指针变量的大小就是8个字节。#includestdio.h//指针变量的⼤⼩取决于地址的⼤⼩//32位平台下地址是32个bit位即4个字节//64位平台下地址是64个bit位即8个字节intmain(){printf(%zd\n,sizeof(char*));printf(%zd\n,sizeof(short*));printf(%zd\n,sizeof(int*));printf(%zd\n,sizeof(double*));return0;}X86环境输出结果X64环境输出结果结论● 32位平台下地址是32个bit 位指针变量大小是4个字节● 64位平台下地址是64个bit 位指针变量大小是8个字节● 注意指针变量的大小和类型是无关的只要指针类型的变量在相同的平台下大小都是相同的。三、指针变量类型的意义指针变量的大小和类型无关只要是指针变量在同一个平台下大小都是一样的为什么还要有各 种各样的指针类型呢?其实指针类型是有特殊意义的我们接下来继续学习。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* 的指针的解引用就能访问四个字节。2 .指针±整数先看一段代码调试观察地址的变化。#includestdio.hintmain(){intn10;char*pc(char*)n;int*pin;printf(n %p\n,n);printf(pc %p\n,pc);printf(pc 1 %p\n,pc1);printf(pi %p\n,pi);printf(pi 1 %p\n,pi1);return0;}代码运行的结果如下我们可以看出char* 类型的指针变量1跳过1个字节int* 类型的指针变量1跳过了4个字节。这就是指针变量的类型差异带来的变化。指针1,其实跳过1个指针指向的元素。指针可以1,那也可 以-1。结论指针的类型决定了指针向前或者向后走一步有多大(距离)。3. void*指针在指针类型中有一种特殊的类型是void*类型的可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针**可以用来接受任意类型地址。**但是也有局限性void*类型的指针不能直接进 行指针的±整数和解引用的运算。举例#includestdio.hintmain(){inta10;int*paa;char*pca;return0;}在上面的代码中将一个int 类型的变量的地址赋值给一个char* 类型的指针变量。编译器给出了一个警 告(如下图),是因为类型不兼容。而使用void* 类型就不会有这样的问题。VS编译的结果:使用void* 类型的指针接收地址#includestdio.hintmain(){inta10;void*paa;void*pca;*pa10;*pc0;return0;}VS编译代码的结果这里我们可以看到void* 类型的指针可以接收不同类型的地址但是无法直接进行指针运算。那么 void* 类型的指针到底有什么用呢?一般 void* 类型的指针是使用在函数参数的部分用来接收不同类型数据的地址这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据。四、指针运算指针的基本运算有三种分别是● 指针±整数● 指针-指针● 指针的关系运算指针的运算规则是通过指针类型决定的4. 指针±整数因为数组在内存中是连续存放的只要知道第一个元素的地址顺藤摸瓜就能找到后面的所有元素。指针的运算规则是通过指针类型决定的如下intarr[10]{1,2,3,4,5,6,7,8,9,10};数组元素和下标#includestdio.h//指针- 整数intmain(){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));//pi 这⾥就是指针整数}return0;}由于int类型大小占4个字节指针每加1往后移动4个字节同理char类型的指针每加1往后移动1个字节2. 指针-指针指针-指针可以用来计算两个地址间的元素个数同样指针类型决定了几个字节看作一个元素如下。//指针-指针#includestdio.hintmy_strlen(char*s)//char类型1个字节1元素{char*ps;while(*p!\0)p;returnp-s;//指针—-指针返回元素个数}intmain(){printf(%d\n,my_strlen(abc));return0;}上图中我们自己定义了一个函数用于求元素个数每检查一个元素加一每检查一个函数加一直到检查到’\0’停下然后用最终地址-初始地址求得元素个数。3. 指针的关系运算指针也可以用于关系运算如下intmain(){intarr[10]{1,2,3,4,5,6,7,8,9,10};int*parr[0];intszsizeof(arr)/sizeof(arr[0]);while(parrsz)//指针的⼤⼩⽐较{printf(%d ,*p);p;}return0;}如图上如果p arr sz成立返回1为真如果p arr sz不成立返回0为假。五、const 修饰指针1. const 修饰变量变量是可以修改的如果把变量的地址交给一个指针变量通过指针变量的也可以修改这个变量。 但是如果我们希望一个变量加上一些限制不能被修改怎么做呢?#includestdio.hintmain(){intm0;m20;//m是可以修改的constintn0;n20;//n是不能被修改的return0;}上述代码中n 是不能被修改的其实n 本质是变量只不过被const 修饰后在语法上加了限制只要我们在代码中对n就行修改就不符合语法规则就报错致使没法直接修改n。但是如果我们绕过n, 使用n的地址去修改n 就能做到了虽然这样做是在打破语法规则。#includestdio.hintmain(){constintn0;printf(n %d\n,n);int*pn;*p20;printf(n %d\n,n);return0;}输出结果我们可以看到这里一个确实修改了。但是我们还是要思考一下为什么n 要被const 修饰呢?就是为了 不能被修改如果p拿到n的地址就能修改n, 这样就打破了const 的限制这是不合理的所以应该让p拿到n的地址也不能修改n, 那接下来怎么做呢?2. const 修饰指针变量一般来讲const 修饰指针变量可以放在的左边也可以放在的右边意义是不一样的。int * p;//没有const修饰 int const * p;//const 放在*的左边做修饰 int * const p;//const 放在*的右边做修饰我们看下面代码来分析具体分析一下结论const 修饰指针变量的时候const 如果放在*的左边修饰的是指针指向的内容保证指针指向的内容不能通过指针来改变。 但是指针变量本身的内容可变。const如果放在*的右边修饰的是指针变量本身保证了指针变量的内容不能修改但是指针指 向的内容可以通过指针改变。六、野指针概念 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)1. 野指针成因1)指针未初始化#includestdio.hintmain(){int*p;//局部变量指针未初始化默认为随机值*p20;return0;}2)指针越界访问#includestdio.hintmain(){intarr[10]{0};int*parr[0];inti0;for(i0;i11;i){//当指针指向的范围超出数组arr的范围时p就是野指针*(p)i;}return0;}上图中arr[10]的最后一位为arr[9]而for循环将arr[10]arr[11]也修改了。3)指针指向的空间释放#includestdio.hint*test(){intn100;returnn;}//该函数进程完成后空间释放给系统intmain(){int*ptest();printf(%d\n,*p);return0;}函数进程完成后空间释放给系统test()所占空间被销毁因此p指向的是随机值。2. 如何规避野指针1)指针初始化如果明确知道指针指向哪里就直接赋值地址如果不知道指针应该指向哪里可以给指针赋值NULL.NULL是C语言中定义的一个标识符常量值是00也是地址这个地址是无法使用的读写该地址会报错。初始化如下#includestdio.hintmain(){intnum10;int*p1num;int*p2NULL;return0;}2)小心指针越界一个程序向内存申请了哪些空间通过指针也就只能访问哪些空间不能超出范围访问超出了就是越界访问。3)指针变量不再使用时及时置NULL, 指针使用之前检查有效性当指针变量指向一块区域的时候我们可以通过指针访问该区域后期不再使用这个指针访问空间的 时候我们可以把该指针置为NULL 。因为约定俗成的一个规则就是只要是NULL指针就不去访问 同时使用指针之前可以判断指针是否为NULL。我们可以把野指针想象成野狗野狗放任不管是非常危险的所以我们可以找一棵树把野狗拴起来 就相对安全了给指针变量及时赋值为NULL, 其实就类似把野狗栓起来就是把野指针暂时管理起来。不过野狗即使拴起来我们也要绕着走不能去挑逗野狗有点危险对于指针也是在使用之前我们也要判断是否为NULL, 看看是不是被拴起来的野狗如果是不能直接使用如果不是我们再去使用。intmain(){intarr[10]{1,2,3,4,5,6,7,8,9,10};int*parr[0];inti0;for(i0;i10;i){*(p)i;}//此时p已经越界了可以把p置为NULLpNULL;//下次使⽤的时候判断p不为NULL的时候再使⽤//...parr[0];//重新让p获得地址if(p!NULL)//判断{//...}return0;}4)避免返回局部变量的地址如造成野指针的第3个例子不要返回局部变量的地址。七、assert 断言assert.h头文件定义了宏 assert() , 用于在运行时确保程序符合指定条件如果不符合就报 错终止运行。这个宏常常被称为“断言”。assert(p ! NULL);上面代码在程序运行到这一行语句时验证变量 p 是否等于 NULL 。如果确实不等于 NULL , 程序 继续运行否则就会终止运行并且给出报错信息提示。assert() 宏接受一个表达式作为参数。如果该表达式为真, assert() 不会产生 任何作用程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错在标准错误流 stderr 中写入一条错误信息显示没有通过的表达式以及包含这个表达式的文件名和行号。#includeassert.hintmain(){intarr[10]{1,2,3,4,5,6,7,8,9,10};int*parr[0];inti0;for(i0;i10;i){*(p)i;}//此时p已经越界了可以把p置为NULLpNULL;//下次使⽤的时候判断p不为NULL的时候再使⽤//...assert(p!NULL);//判断{//...}return0;}使用assert()有 几个好处它不仅能自动标识文件和出问题的行号还有一种无需更改代码就能开启或关闭assert()的机制。如果已经确认程序没有问 题不需要再做断言就在 #include assert.h 语句的前面定义一个宏 NDEBUG。然后重新编译程序编译器就会禁用文件中所有的 assert() 语句。如果程序又出现问题可以移除这条 #define NDEBUG 指令(或者把它注释掉),再次编译这样就重新启用了 assert()语句。#define NDEBUG #include assert.hassert()的缺点是因为引入了额外的检查增加了程序的运行时间。一般我们可以在 Debug 中使用在 Release版本中选择禁用 assert 就行在 VS 这样的集成开发环境中在 Release版本中直接就是优化掉了。这样在debug 版本写有利于程序员排查问题在 Release版本不影响用户使用时程序的效率。八、指针的传址调用1. 传值调用和传址调用学习指针的目的是使用指针解决问题那什么问题非指针不可呢?例如写一个函数交换两个整型变量的值一番思考后我们可能写出这样的代码#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 和b,a 的地址是0×00cffdd0,b 的地址是0×00cffdc4, 在调用Swap1函数时将a和b传递给了Swap1函数在Swap1函数内部创建了形参x和y接收a和b的值但是 x的地址是0x00cffcec,y 的地址是0x00cffcf0,x和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*px,int*py){inttmp0;tmp*px;*px*py;*pytmp;}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 函数的时候是将变量的地址传 递给了函数这种函数调用方式叫传址调用。传址调用可以让函数和主调函数之间建立真正的联系在函数内部可以修改主调函数中的变量所以未来函数中只是需要主调函数中的变量值来实现计算就可以采用传值调用。如果函数内部要修改主调函数中的变量的值就需要传址调用。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表