【C语言】深度剖析指针(一):内存、地址与指针操作 文章目录一、内存和地址1.1 内存的基本概念1.2 编址的原理二、指针变量和地址2.1 取地址操作符2.2 指针变量和解引用操作符*2.2.1 指针变量2.2.2 指针类型的解读2.2.3 解引用操作符2.3 指针变量的大小三、指针变量类型的意义3.1 影响解引用的权限3.2 影响指针 ± 整数的步长3.3 void* 指针四、const修饰指针4.1 const修饰变量4.2 const修饰指针变量五、指针运算5.1 指针±整数5.2 指针-指针5.3 指针的关系运算六、野指针6.1 野指针成因6.2 如何规避野指针七、assert断言八、指针的使用和传址调用8.1 strlen的模拟实现8.2 传值调用和传址调用一、内存和地址1.1 内存的基本概念在计算机中CPU处理数据时需要从内存中读取数据处理后的数据也会放回内存。为了高效管理内存内存被划分为一个个大小为1字节的内存单元每个内存单元都有唯一的编号这个编号就是我们所说的地址在C语言中也被称为指针。举个例子一栋宿舍楼有100个房间给每个房间编上号如101、102等根据房间号就能快速找到房间。内存就像这栋宿舍楼每个内存单元就像一个房间地址则是房间号有了地址CPU就能快速找到对应的内存单元。计算机中常见的存储单位及换算关系如下1byte字节 8bit比特位1KB 1024byte1MB 1024KB1GB 1024MB1TB 1024GB1PB 1024TB1.2 编址的原理CPU访问内存中的某个字节空间必须知道该字节空间的地址。计算机的编址是通过硬件设计实现的。CPU和内存之间通过大量的线连接其中一组重要的线是地址总线。32位机器有32根地址总线每根线有0和1两种状态表示电脉冲有无32根地址线能表示2^32种不同的地址。地址信息通过地址总线传给内存内存根据地址找到对应数据再通过数据总线传入CPU寄存器。二、指针变量和地址2.1 取地址操作符在C语言中创建变量的本质是向内存申请空间。#includestdio.hintmain(){inta10;return0;}变量a占用4个字节的内存空间每个字节都有自己的地址。我们可以使用取地址操作符来获取变量的地址如a得到的是a所占4个字节中地址较小的那个字节的地址。2.2 指针变量和解引用操作符*2.2.1 指针变量通过取地址操作符得到的地址是一个数值我们可以将其存储在指针变量中。指针变量就是专门用来存放地址的变量。#includestdio.hintmain(){inta10;int*paa;// 取出a的地址并存储到指针变量pa中return0;}2.2.2 指针类型的解读指针变量的类型由*和前面的类型组成如int*表示该指针变量指向的是整型int类型的对象。对于char类型的变量ch其地址应存放在char*类型的指针变量中。2.2.3 解引用操作符有了指针变量存储地址后我们可以使用解引用操作符*通过地址找到对应的变量并进行操作。#includestdio.hintmain(){inta100;int*paa;*pa0;// 通过pa中存放的地址找到a并将a的值改为0return0;}这里*pa就相当于变量a通过*pa可以对a进行修改这为操作变量提供了另一种途径。2.3 指针变量的大小指针变量的大小取决于地址的大小在32位平台下地址是32个比特位指针变量大小为4个字节。在64位平台下地址是64个比特位指针变量大小为8个字节。需要注意的是指针变量的大小和其类型无关在相同平台下所有指针类型的变量大小都是相同的。例如#includestdio.hintmain(){printf(%zd\n,sizeof(char*));printf(%zd\n,sizeof(short*));printf(%zd\n,sizeof(int*));printf(%zd\n,sizeof(double*));return0;}在32位环境下输出结果均为4在64位环境下输出结果均为8。三、指针变量类型的意义虽然指针变量的大小和类型无关但指针类型有着重要的意义。3.1 影响解引用的权限指针的类型决定了对指针解引用时的操作权限即一次能操作的字节数。例如char*类型的指针解引用只能访问1个字节。int*类型的指针解引用能访问4个字节。看下面两段代码// 代码1intmain(){inta0x11223344;int*pa;*p0;return0;}逐语句调试代码运行到15行时。int*可以访问四个字节将四个字节都改为0// 代码2intmain(){inta0x11223344;char*pa;*p0;return0;}逐语句调试代码运行到22行时。char*只访问访问一个字节将第一个字节改为03.2 影响指针 ± 整数的步长指针的类型决定了指针向前或者向后走一步的距离。例如int* 1跳过四个字节int大小char* 1跳过一个字节char大小3.3 void* 指针void*类型的指针可以接受任意类型的地址可以理解为无具体类型指针或者叫泛型指针但它不能直接进行指针的±整数和解引用运算。通常用于函数参数部分实现泛型编程的效果以处理多种类型的数据。四、const修饰指针4.1 const修饰变量被const修饰的变量不能直接被修改。#includestdio.hintmain(){constintn0;n20;// 报错n不能被直接修改return0;}此时变量具有常属性称为常变量但本质依旧是变量而不是常量。在C中被const修饰则为常量。但如果通过指针获取其地址还是可以修改该变量的值这显然打破了const的限制所以需要用const修饰指针变量。4.2 const修饰指针变量const放在*的右边修饰的是指针变量本身指针变量不可以再指向其他变量。但可以通过指针修改指向的内容。intmain(){inta10;intb20;int*constpa;pb;//err*p100;//可以通过编译return0;}const放在*的左边限制指向的内容不可以通过指针来修改但可以修改指针指向的变量。intmain(){inta10;intb20;intconst*pa;pb;//可以通过编译*p100;//errreturn0;}五、指针运算指针的基本运算有三种5.1 指针±整数原理同本文3.2部分。由于数组在内存中是连续存放的知道第一个元素的地址后通过指针±整数可以访问数组中的其他元素。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));// 通过指针整数访问数组元素}return0;}注意*(p1)不要写成*p1前者表示指针变量1后者表示p指向的内容1在sizeof()中输入数组名arr计算整个数组的大小。5.2 指针-指针通过上述我们可以明确指针1 整数 指针2以此推理出整数 指针2 - 指针1类比“日期 - 日期”得到之间的天数。两个指针相减的结果是它们之间的元素个数常用于计算字符串长度等场景。strlen()求字符串长度统计字符串\0之前字符个数数组名arr是数组首元素的地址。arr等价于arr[0]模拟实现strlen()函数方法1 计数器intmy_strlen(char*s){intcnt0;//计数器while(*s \0){cnt;str}returncnt;// 计算两个指针之间的元素个数即字符串长度}intmain(){printf(%d\n,my_strlen(abc));//输出3return0;}方法2 指针 - 指针intmy_strlen(char*s){char*ps;while(*p!\0)p;returnp-s;// 计算两个指针之间的元素个数即字符串长度}intmain(){printf(%d\n,my_strlen(abc));//输出3return0;}注意指针 - 指针 的前提时两个指针指向同一块空间例如intmain(){intarr[10]{0};charch[10]{0};printf(%d\n,ch[0]-arr[0]);//errreturn0;}日期 日期没有意义同样的“指针 指针”也没有任何意义。5.3 指针的关系运算指针与指针比较大小其实就是地址与地质比较大小。数组随下标变大地址由低变高。intmain(){intarr[10]{1,2,3,4,5,6,7,8,9,10};int*parr[0];intszsizeof(arr)/sizeof(arr[0]);while(parrsz)// 指针的大小比较当p指向的地址小于arrsz(相当于数组最后一个元素的地址)进入循环{printf(%d ,*p);p;}//输出 1 2 3 4 5 6 7 8 9 10return0;}六、野指针野指针就是指针指向内容是不可知的不正确、随机、没有明确限制6.1 野指针成因指针未初始化局部变量指针未初始化时其值是随机的。intmain(){int*p;//此时p是局部变量指针未初始化默认为随机值*p20;return0;}指针越界访问指针指向的范围超出数组等申请的内存空间。intmain(){intarr[10]{0};int*parr[0];inti0;for(i0;i11;i){//当指针指向的范围超出数组arr的范围时p就是野指针*(p)i;}return0;}指针指向的空间释放返回局部变量的地址该局部变量的空间在函数调用结束后会被释放。int*test(){intn100;returnn;}intmain(){int*ptest();printf(%d\n,*p);//此时test()调用完成栈帧被销毁 内存被释放return0;}6.2 如何规避野指针指针初始化明确指向时直接赋值地址否则赋值NULLNULL是值为0的标识符常量该地址无法使用,读写地址也会报错。intmain(){int*pNULL;*p20//err}小心指针越界不访问超出申请内存范围的空间。及时置NULL并检查指针变量不再使用时置为NULL使用前判断是否为NULL。避免返回局部变量的地址。七、assert断言assert.h头文件中的assert()宏用于在运行时确保程序符合指定条件如果不符合就报错终止运行。其表达式为真时程序继续运行为假时报错并显示相关信息。assertp!NULL;//确保 p为有效指针assert()断言相对if语句的优点出现错误会直接报错指明在什么文件哪一行无需修改代码就可以禁用assert().可以通过在#include assert.h前定义NDEBUG宏来禁用assert()语句.缺点引入了额外的检查增加了程序运行时间通常在Debug版本中使用Release版本中禁用以不影响程序效率。VS2022中release版会直接禁用assert八、指针的使用和传址调用8.1 strlen的模拟实现strlen函数用于求字符串长度统计的是字符串中\0之前的字符个数。模拟实现如下#includestdio.h#includeassert.hsize_tmy_strlen(constchar*str)//限制内容不能被修改{intcount0;assert(str);// 确保 str不为NULLwhile(*str){count;str;}returncount;}intmain(){intlenmy_strlen(abcdef);printf(%zd\n,len);return0;}求出的长度不可能是负数因此返回值类型使用size_t无符号整型更合适打印应使用zd%作为占位符。8.2 传值调用和传址调用传值调用实参传递给形参时形参创建临时空间接收实参。形参和实参是独立的两个空间。形参只是实参的一份临时拷贝对形参的修改不影响实参。传址调用将变量的地址传递给函数函数内部通过地址间接操作主调函数中的变量可实现对变量的修改。例如交换两个整型变量的值使用传址调用#includestdio.hvoidSwap(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);Swap(a,b);printf(交换后:a%d b%d\n,a,b);return0;}运行结果