第一章 数据在计算机内存中真实的存储形式
1.1 变量存储的底层逻辑
我们写代码定义变量,本质就是向操作系统申请一块内存空间,给这块空间起一个变量名,再把我们写的十进制数字放进内存里。 计算机只能识别 0 和 1 组成的二进制,我们写的十进制数字,必须先转为二进制格式,才能存入内存。
十进制转二进制通用方法:除 2 取余法
- 数字不停除以 2,每一次运算记录下余数(余数只有 0 或者 1)
- 一直计算,直到除法得到的结果(商)等于 0 就停止计算
- 把所有记录的余数,从最后一个写到第一个,拼接起来就是对应二进制数
日常计算嫌麻烦,可以打开电脑自带计算器,切换成程序员模式,直接完成二进制、八进制、十进制、十六进制互相转换。
举例:十进制数字 10,对应二进制是1010int类型固定占用 4 字节内存,1 字节 = 8 个二进制位(bit),4 字节就是 32bit。需要把二进制补齐 32 位存放,最终内存存放格式:00000000 00000000 00000000 00001010
long long类型固定占用 8 字节(64bit),数字 123456 补齐 64 位二进制后存入内存。
1.2 数组在内存的存储规则
数组是一片连续无空隙的内存空间,相同类型的元素挨个排列存放。 示例:int arr [3];数组一共 3 个 int 元素,单个 int 占 4 字节,总占用内存:3 × 4 = 12 字节 逐个赋值:arr [0]=1,arr [1]=2,arr [2]=3
- arr [0] 占用:第 1~4 字节
- arr [1] 占用:第 5~8 字节
- arr [2] 占用:第 9~12 字节
每个 int 数值都会补齐 32 位二进制,对应写入自己所属的 4 字节空间。
1.3 内存查看为什么显示十六进制,不直接显示二进制
1 个十六进制字符刚好对应 4 位二进制,两个十六进制字符就能完整代表 1 字节内存。 二进制字符串太长,人工查看、对比、查找地址极易出错,十六进制写法更简洁,所以调试内存时统一展示十六进制数据。
例子:二进制00001111 00010101 10101100 11110000,简写为十六进制:0F15ACF0
1.4 VS 软件断点查看内存的实操步骤
- 在代码行左侧灰色区域点击,出现红色圆点代表打上断点;程序运行到断点位置会暂停,方便观察静态内存数据
- 顶部菜单栏选择【调试】→【窗口】→【内存】,打开任意内存窗口
- 在内存地址输入框填写
&变量名,按下回车,就能看到该变量内存原始的十六进制数据
例:int a=10,断点查看内存,4 字节整体数据为00 00 00 0A
1.5 进制快速对照表(0~15)
表格
| 十进制数值 | 补齐 4 位二进制 | 对应十六进制 |
|---|---|---|
| 0 | 0000 | 0 |
| 1 | 0001 | 1 |
| 2 | 0010 | 2 |
| 3 | 0011 | 3 |
| 4 | 0100 | 4 |
| 5 | 0101 | 5 |
| 6 | 0110 | 6 |
| 7 | 0111 | 7 |
| 8 | 1000 | 8 |
| 9 | 1001 | 9 |
| 10 | 1010 | A |
| 11 | 1011 | B |
| 12 | 1100 | C |
| 13 | 1101 | D |
| 14 | 1110 | E |
| 15 | 1111 | F |
第二章 大小端存储(内存字节排布规则)
2.1 大小端出现的现象
我们手动换算的数值十六进制,和 VS 断点查到的内存字节顺序经常是颠倒的,Windows 系统默认使用小端存储模式。 大端模式多用在网络传输、单片机、ARM 架构设备中。
2.2 大小端标准定义
我们以数值int num = 0x12345678举例,4 字节拆分:高字节0x12、次高字节0x34、次低字节0x56、低字节0x78
✅小端存储规则:低字节放在内存低地址,高字节放在内存高地址(内存里字节顺序倒着放) 假设变量起始地址为0x00010000
表格
| 内存地址 | 存放字节 | 字节属性 |
|---|---|---|
| 0x00010000(最低地址) | 78 | 低字节 |
| 0x00010001 | 56 | 次低字节 |
| 0x00010002 | 34 | 次高字节 |
| 0x00010003(最高地址) | 12 | 高字节 |
内存窗口展示顺序:78 56 34 12
✅大端存储规则:高字节放在内存低地址,低字节放在内存高地址(字节顺序和我们书写数字完全一致)
表格
| 内存地址 | 存放字节 | 字节属性 |
|---|---|---|
| 0x00010000(最低地址) | 12 | 高字节 |
| 0x00010001 | 34 | 次高字节 |
| 0x00010002 | 56 | 次低字节 |
| 0x00010003(最高地址) | 78 | 低字节 |
内存窗口展示顺序:12 34 56 78
2.3 CPU 架构和指针大小关系
X86(32 位系统)、X64(64 位系统):英特尔、AMD 电脑 CPU 通用架构,Windows、Linux 系统都是小端存储 手机骁龙、麒麟芯片属于 ARM 架构
架构对指针的唯一影响:X86 环境下指针固定占 4 字节,X64 环境下指针固定占 8 字节,本讲义全部以 X86(32 位)讲解。
2.4 代码自测本机是大端还是小端(可直接复制运行)
#include <stdio.h> void check_endian() { int num = 0x1234; char *p = (char*)# //强制转换,截取最低1字节查看 if(*p == 0x34) printf("当前电脑为小端存储\n"); else printf("当前电脑为大端存储\n"); } int main() { check_endian(); return 0; }第三章 地址、取地址符 & 与指针基础入门
3.1 什么是内存地址
计算机给每一个字节的内存,都分配了独一无二的编号,这个编号就叫做内存地址(用十六进制展示)。 一块连续的内存,地址编号从小到大依次递增;只要拿到一块内存第一个字节的地址,结合数据类型占用的字节数量,就能精准锁定整片内存。
定义int a=10;,变量 a 占用连续 4 个字节,四个字节各自都有独立地址,&a获取的是这 4 个字节里第一个字节的地址编号。 格式示例(0x代表后面数字是十六进制):
0x00000001 → 存放 0A 0x00000002 → 存放 00 0x00000003 → 存放 00 0x00000004 → 存放 003.2 取地址符 & 用法
书写格式:& + 变量名作用:取出变量对应内存首字节的地址编号 打印地址需要使用格式化符号%p
3.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 *p | char 变量、字符数组 | 1 字节 | 查看原始内存字节、判断大小端 |
| 短整型指针 | short *p | short 变量 | 2 字节 | 操作短整数数据 |
| 整型指针 | int *p | int 变量 | 4 字节 | 数组遍历、函数地址传参(最常用) |
| 长长整型指针 | long long *p | long long 变量 | 8 字节 | 存储超大数值 |
| 单精度浮点指针 | float *p | float 浮点数据 | 4 字节 | 处理小数 |
| 双精度浮点指针 | double *p | double 高精度小数 | 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 指针合法运算与绝对禁忌
✅合法运算
- 指针 ± 整数:
p+n、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:主动给指针赋值 NULL,NULL 本质是系统保护的 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 a=1,b=2; swap_val(a,b); printf("a=%d,b=%d",a,b); //输出a=1 b=2,数值不会交换 return 0; }类比:把作文复印一份交给老师修改,复印件的改动不会修改你手里的原稿。
7.2 地址传递
传递的是变量的内存地址,函数内部通过解引用直接操作原始内存,外部变量的数据会被同步修改。 案例:正确的数字交换写法
void swap_addr(int *x, int *y) { int tmp = *x; *x = *y; *y = tmp; } int main() { int a=1,b=2; swap_addr(&a,&b); printf("a=%d,b=%d",a,b); //输出a=2 b=1,交换成功 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*;arr+1只会跳过 1 个 int 大小(4 字节)
补充知识点:数组下标[]底层本质 编译器不存在[]下标运算符,所有arr[i]都会被编译为*(arr+i);加法满足交换律,*(arr+i)等价*(i+arr),所以写法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)
习题 1:long long arr [10] = {1,2,3,4,5,6,7,8,9,10};习题 2:char arr [5] = {97,98,99,100,101};习题 3:char arr [16] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
解析统一逻辑:
sizeof(arr)→ 数组整体字节 = 单个元素字节 × 元素个数sizeof(&arr)→ 数组指针,固定 4 字节(X86)sizeof(*arr)→ 解引用后为数组第一个元素,等于对应数据类型本身字节arr+1:按照元素类型字节偏移地址;&arr+1:一次性跳过整个数组的总字节*arr获取的是数组第一个元素真实数值,数值做 + 1 就是普通数字加法,不再是指针运算- 不同类型指针接收同一数组首地址,指针 + N 的偏移字节 = N × 指针对应类型字节大小,结合小端存储读取对应数值
9.2 二维数组相关计算习题(习题 5、习题 6、习题 7)
二维数组本质:数组里面的每一个元素,又是一个一维数组 二维数组名 arr 普通场景退化:类型为int(*)[列数],arr+1会跳过一整行所有元素的总字节&arr代表整个二维数组的地址,&arr+1直接跳过整个二维数组全部字节 多级解引用**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完整数组地址p+1:一次性偏移 40 字节(10 个 int 总大小)&arr [0]+1:只偏移 4 字节,单个 int 大小&arr+1:一次性偏移 40 字节,整个数组总大小