ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

11.初级指针

11.初级指针 第一章 数据在计算机内存中真实的存储形式1.1 变量存储的底层逻辑我们写代码定义变量本质就是向操作系统申请一块内存空间给这块空间起一个变量名再把我们写的十进制数字放进内存里。 计算机只能识别 0 和 1 组成的二进制我们写的十进制数字必须先转为二进制格式才能存入内存。十进制转二进制通用方法除 2 取余法数字不停除以 2每一次运算记录下余数余数只有 0 或者 1一直计算直到除法得到的结果商等于 0 就停止计算把所有记录的余数从最后一个写到第一个拼接起来就是对应二进制数日常计算嫌麻烦可以打开电脑自带计算器切换成程序员模式直接完成二进制、八进制、十进制、十六进制互相转换。举例十进制数字 10对应二进制是1010int类型固定占用 4 字节内存1 字节 8 个二进制位bit4 字节就是 32bit。需要把二进制补齐 32 位存放最终内存存放格式00000000 00000000 00000000 00001010long long类型固定占用 8 字节64bit数字 123456 补齐 64 位二进制后存入内存。1.2 数组在内存的存储规则数组是一片连续无空隙的内存空间相同类型的元素挨个排列存放。 示例int arr [3];数组一共 3 个 int 元素单个 int 占 4 字节总占用内存3 × 4 12 字节 逐个赋值arr [0]1arr [1]2arr [2]3arr [0] 占用第 14 字节arr [1] 占用第 58 字节arr [2] 占用第 912 字节每个 int 数值都会补齐 32 位二进制对应写入自己所属的 4 字节空间。1.3 内存查看为什么显示十六进制不直接显示二进制1 个十六进制字符刚好对应 4 位二进制两个十六进制字符就能完整代表 1 字节内存。 二进制字符串太长人工查看、对比、查找地址极易出错十六进制写法更简洁所以调试内存时统一展示十六进制数据。例子二进制00001111 00010101 10101100 11110000简写为十六进制0F15ACF01.4 VS 软件断点查看内存的实操步骤在代码行左侧灰色区域点击出现红色圆点代表打上断点程序运行到断点位置会暂停方便观察静态内存数据顶部菜单栏选择【调试】→【窗口】→【内存】打开任意内存窗口在内存地址输入框填写变量名按下回车就能看到该变量内存原始的十六进制数据例int a10断点查看内存4 字节整体数据为00 00 00 0A1.5 进制快速对照表0~15表格十进制数值补齐 4 位二进制对应十六进制000000100011200102300113401004501015601106701117810008910019101010A111011B121100C131101D141110E151111F第二章 大小端存储内存字节排布规则2.1 大小端出现的现象我们手动换算的数值十六进制和 VS 断点查到的内存字节顺序经常是颠倒的Windows 系统默认使用小端存储模式。 大端模式多用在网络传输、单片机、ARM 架构设备中。2.2 大小端标准定义我们以数值int num 0x12345678举例4 字节拆分高字节0x12、次高字节0x34、次低字节0x56、低字节0x78✅小端存储规则低字节放在内存低地址高字节放在内存高地址内存里字节顺序倒着放 假设变量起始地址为0x00010000表格内存地址存放字节字节属性0x00010000最低地址78低字节0x0001000156次低字节0x0001000234次高字节0x00010003最高地址12高字节内存窗口展示顺序78 56 34 12✅大端存储规则高字节放在内存低地址低字节放在内存高地址字节顺序和我们书写数字完全一致表格内存地址存放字节字节属性0x00010000最低地址12高字节0x0001000134次高字节0x0001000256次低字节0x00010003最高地址78低字节内存窗口展示顺序12 34 56 782.3 CPU 架构和指针大小关系X8632 位系统、X6464 位系统英特尔、AMD 电脑 CPU 通用架构Windows、Linux 系统都是小端存储 手机骁龙、麒麟芯片属于 ARM 架构架构对指针的唯一影响X86 环境下指针固定占 4 字节X64 环境下指针固定占 8 字节本讲义全部以 X8632 位讲解。2.4 代码自测本机是大端还是小端可直接复制运行#include stdio.h void check_endian() { int num 0x1234; char *p (char*)num; //强制转换截取最低1字节查看 if(*p 0x34) printf(当前电脑为小端存储\n); else printf(当前电脑为大端存储\n); } int main() { check_endian(); return 0; }第三章 地址、取地址符 与指针基础入门3.1 什么是内存地址计算机给每一个字节的内存都分配了独一无二的编号这个编号就叫做内存地址用十六进制展示。 一块连续的内存地址编号从小到大依次递增只要拿到一块内存第一个字节的地址结合数据类型占用的字节数量就能精准锁定整片内存。定义int a10;变量 a 占用连续 4 个字节四个字节各自都有独立地址a获取的是这 4 个字节里第一个字节的地址编号。 格式示例0x代表后面数字是十六进制0x00000001 → 存放 0A 0x00000002 → 存放 00 0x00000003 → 存放 00 0x00000004 → 存放 003.2 取地址符 用法书写格式 变量名作用取出变量对应内存首字节的地址编号 打印地址需要使用格式化符号%p3.3 指针的本质指针本质就是一块独立的内存空间专门用来存放其他变量的内存首地址。 指针书写格式数据类型 * 指针名一般习惯用 p 命名指针 指针自身也占用内存X86 为 4 字节X64 为 8 字节基础代码示例int a 11; int* p a; printf(a的地址%p\n, a); printf(指针p存储的地址%p\n, p);两段打印结果完全一致a的类型本身就是int*。 指针 p 自己有专属的内存地址p 的内存里保存着变量 a 的首地址数据数据存放同样遵循小端规则。3.4 二级指针指针的指针一级指针存放普通变量的地址二级指针存放一级指针变量自身的地址。int a 10; int *p a; //一级指针p存储变量a的地址 int **pp p; //二级指针pp存储一级指针p自身的地址第四章 解引用符号 * 与指针实际应用4.1 解引用 * 的底层规则格式*指针变量名作用顺着指针保存的地址向后读取【指针对应类型大小】的字节数据转换成对应类型的值也可以通过地址修改原始内存里的数据。4.2 解引用两大核心用法读取原始变量的数据int a 10; int* p a; printf(%d, *p); //顺着地址读取a内存的数据输出10 //只打印p只能看到一串十六进制地址拿不到真实数值修改原始变量内存数据地址传递的核心原理int a 10; int* p a; *p 999; //通过地址直接覆盖a所在内存的值 printf(%d, a); //输出结果999原理函数内部使用指针解引用操作的是变量原始内存所以可以修改外部变量的值。4.3 不同类型指针作用对照表表格指针类型写法指向内容X86 下解引用读取字节数日常使用场景字符指针char *pchar 变量、字符数组1 字节查看原始内存字节、判断大小端短整型指针short *pshort 变量2 字节操作短整数数据整型指针int *pint 变量4 字节数组遍历、函数地址传参最常用长长整型指针long long *plong long 变量8 字节存储超大数值单精度浮点指针float *pfloat 浮点数据4 字节处理小数双精度浮点指针double *pdouble 高精度小数8 字节高精度浮点运算无类型指针void *p任意类型地址不能直接解引用通用地址容器、万能函数参数补充void* 可以保存任意类型地址但不能直接解引用也不能直接做指针加减运算使用前必须强制类型转换。第五章 const 修饰指针笔试必考 指针运算规则5.1 const 修饰指针三种写法区分口诀const 修饰谁谁就不能修改const int *p等价int const *p限制不能通过*p修改指针指向的数据但是指针 p 可以更换指向别的变量 使用场景函数接收数组 / 变量只读取数据禁止修改原始内容#include stdio.h int main() { int a 10, b 20; const int *p a; //*p 99; 编译报错无法修改数据 p b; //合法指针可以更换指向 printf(%d, *p); return 0; }int *const p限制指针初始化绑定地址后永远不能修改指向但是可以用*p修改指向的数据 使用场景固定指针指向一块内存防止代码误改动指针地址int main() { int a 10; int *const p a; *p 99; //合法修改数据没问题 //p NULL; 编译报错指针地址被锁定 printf(%d, a); return 0; }const int *const p限制指针指向不能改、指向的数据也不能改双向只读 使用场景读取常量字符串这类只读内存数据int main() { int a 10; const int *const p a; //*p 99; 报错 //p NULL; 报错 return 0; }5.2 指针合法运算与绝对禁忌✅合法运算指针 ± 整数pn、p-n实际移动字节 数字 × 指针指向类型的字节大小数组遍历的底层原理⚠️注意只能在同一块连续内存数组内加减越界访问属于未定义行为。相同类型的指针可以相减两个指针相减得到两个地址中间相隔的元素个数结果为整型数字int arr[10] {1,2,3,4,5}; int *p1 arr[0]; int *p2 arr[4]; printf(%d, p2 - p1); //输出4中间间隔4个int元素指针可以使用、、、!做比较运算判断地址高低❌绝对禁止的运算指针和指针相加、指针相乘、指针相除地址只是一串编号这类运算没有任何实际意义编译器会直接报错。第六章 空指针、野指针、悬空指针新手最常踩坑 BUG把指针理解成记录房间门牌号的小本子空指针 NULL主动给指针赋值 NULLNULL 本质是系统保护的 0 号无效地址。 规则0 地址系统禁止读写数据不会随意篡改内存使用指针前先判断if(p ! NULL)再操作是最安全的指针初始化方式。野指针定义指针的时候没有手动赋值指针内存里自动填充了随机杂乱的内存地址。 危害程序莫名闪退、隐性 bug 极难排查解决方案所有指针定义立刻赋值 NULL 清零。int *p; //未初始化野指针 p NULL; //修复野指针问题悬空指针指针原本保存了合法内存地址但是这块内存被销毁 / 释放了指针本子上还留着失效的门牌号。 产生场景函数运行结束函数内部局部变量自动销毁malloc 申请的内存调用 free 释放。⚠️局部变量函数结束后内存直接回收这块内存随时会被覆盖读取结果不确定未定义行为 危害读取到随机垃圾数据、程序崩溃解决方案内存销毁 / 释放后马上执行p NULL。int* getAddr() { int a 10; //函数执行完毕a的内存直接销毁 return a; } int main() { int *p getAddr(); //p变成悬空指针 printf(%d,*p); //读取垃圾值运行不稳定 return 0; }三类指针极简区分空指针人为主动设置无效地址全程可控、安全野指针忘记初始化产生随机地址被动生成高危错误悬空指针地址曾经合法内存销毁后地址失效属于代码书写疏漏第七章 值传递与地址传递指针最核心使用价值7.1 值传递传递的是变量真实的数值函数内部会复制一份一模一样的数据运行函数修改的只是副本数据不会影响外部原始变量。 案例交换数字写法错误示范void swap_val(int x, int y) { int tmp x; x y; y tmp; } int main() { int a1,b2; swap_val(a,b); printf(a%d,b%d,a,b); //输出a1 b2数值不会交换 return 0; }类比把作文复印一份交给老师修改复印件的改动不会修改你手里的原稿。7.2 地址传递传递的是变量的内存地址函数内部通过解引用直接操作原始内存外部变量的数据会被同步修改。 案例正确的数字交换写法void swap_addr(int *x, int *y) { int tmp *x; *x *y; *y tmp; } int main() { int a1,b2; swap_addr(a,b); printf(a%d,b%d,a,b); //输出a2 b1交换成功 return 0; }类比直接把作业本的家庭地址给老师老师上门修改原本的作业本原稿直接被改动。总结值传递复制数据 → 修改副本 → 外部数据不变 地址传递传递地址 → 操作原内存 → 外部数据被修改第八章 数组和指针的底层关系、数组名三大特性8.1 数组名 ≠ 普通指针变量最容易混淆的知识点数组名是地址常量数组对应的内存位置固定不变数组名只是这片连续内存的别名标签不能被赋值、不能自增自减int arr[5] {1,2,3,4,5}; //arr NULL; 编译报错常量无法修改 //arr; 编译报错普通指针是地址变量指针自己独占一块内存存储地址数值可以随意修改指向、自增自减int *p arr; p NULL; //合法 p; //合法核心两点区分 ①sizeof(数组名)计算整个数组全部的总字节sizeof(指针)固定 4/8 字节只算指针本身大小 ② 数组名不能赋值、自增指针变量完全支持修改指向、自增运算8.2 数组名的三种状态重中之重所有数组计算题的核心定义通用格式int arr [N];数组名放在sizeof()内部数组保留完整数组类型sizeof计算数组整体总字节数 例char arr [100]sizeof(arr)结果 100 字节数组名前面添加取地址获取整个数组的整体地址类型变为数组指针int(*)[N]其余所有普通场景数组名自动退化等价于数组首元素的地址arr[0]类型转为普通一级指针int*arr1只会跳过 1 个 int 大小4 字节补充知识点数组下标[]底层本质 编译器不存在[]下标运算符所有arr[i]都会被编译为*(arri)加法满足交换律*(arri)等价*(iarr)所以写法i[arr]语法合法。⚠️日常开发只用arr[i]书写即可i[arr]只用来理解底层原理工程禁止使用。int arr[5] {10,20,30}; printf(%d %d, arr[1], 1[arr]); //两处输出都是208.3 数组当做函数参数传递数组退化 长度丢失函数形参书写int arr[]和int *arr完全等价编译器全部识别为指针 数组传入函数后数组会退化为普通指针函数内部使用sizeof(arr)只能拿到指针大小4 字节无法计算原始数组真实长度标准解决方案调用函数时把数组长度作为第二个参数手动传入void test(int arr[], int len) { printf(%d\n, sizeof(arr)); //固定输出4 for(int i 0; i len; i) { printf(%d , arr[i]); } } int main() { int arr[5] {1,2,3,4,5}; //在main函数内部计算真实数组长度 int len sizeof(arr)/sizeof(arr[0]); test(arr, len); return 0; }8.4 指针数组 和 数组指针 区分方法优先级规则括号() 方括号[] 星号*指针数组本质是数组数组里面存放的元素全是指针*紧贴[]示例int* arr [10]数组指针本质是指针专门用来存放一整个数组的地址被括号包裹*紧贴变量名 示例int (*p)[10]第九章 配套习题 逐题解析一维数组→二维数组→指针数组9.1 一维数组 sizeof、地址偏移计算题习题 1~ 习题 3习题 1long long arr [10] {1,2,3,4,5,6,7,8,9,10};习题 2char arr [5] {97,98,99,100,101};习题 3char arr [16] {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};解析统一逻辑sizeof(arr)→ 数组整体字节 单个元素字节 × 元素个数sizeof(arr)→ 数组指针固定 4 字节X86sizeof(*arr)→ 解引用后为数组第一个元素等于对应数据类型本身字节arr1按照元素类型字节偏移地址arr1一次性跳过整个数组的总字节*arr获取的是数组第一个元素真实数值数值做 1 就是普通数字加法不再是指针运算不同类型指针接收同一数组首地址指针 N 的偏移字节 N × 指针对应类型字节大小结合小端存储读取对应数值9.2 二维数组相关计算习题习题 5、习题 6、习题 7二维数组本质数组里面的每一个元素又是一个一维数组 二维数组名 arr 普通场景退化类型为int(*)[列数]arr1会跳过一整行所有元素的总字节arr代表整个二维数组的地址arr1直接跳过整个二维数组全部字节 多级解引用**arr逐层抵消指针最终拿到数组第一个真实数据习题 7 强转指针计算核心强制转换只会改变地址的解析方式不会修改地址本身的数值结合ptr-1反向寻址取值9.3 指针数组、多维指针数组计算习题 8、习题 9指针数组内部每个元素都是指针单个指针大小固定 4 字节 多层数组嵌套逐层解引用每一层*都会抵消一层数组 / 指针类型sizeof计算规则只要被sizeof包裹数组就不会退化其余表达式环境数组正常退化和*配对书写arr、*arr地址本身不会发生任何变化只修改数据解析类型9.4 数组指针综合小练习int arr[10]; //普通一维int数组 int* brr[10]; //指针数组数组内存放10个int类型指针brr退化后类型为int** int(*p)[10]; //数组指针专门接收arr完整数组地址p1一次性偏移 40 字节10 个 int 总大小arr [0]1只偏移 4 字节单个 int 大小arr1一次性偏移 40 字节整个数组总大小
返回列表