目录
[1. 内存和地址](#1. 内存和地址)
[1.1 内存](#1.1 内存)
[1.2 究竟该如何理解编址](#1.2 究竟该如何理解编址)
[2. 指针变量和地址](#2. 指针变量和地址)
[2.1 取地址操作符(&)](#2.1 取地址操作符(&))
[2.2 指针变量和解引用操作符(*)](#2.2 指针变量和解引用操作符(*))
[2.2.1 指针变量](#2.2.1 指针变量)
[2.2.2 解引用操作符](#2.2.2 解引用操作符)
[2.3 指针变量的大小](#2.3 指针变量的大小)
[3. 指针变量类型的意义](#3. 指针变量类型的意义)
[3.1 指针的解引用](#3.1 指针的解引用)
[3.2 指针+-整数](#3.2 指针+-整数)
[3.3 void*指针](#3.3 void*指针)
[4. 指针运算](#4. 指针运算)
[4.1 指针+-整数](#4.1 指针+-整数)
[4.2 指针-指针](#4.2 指针-指针)
[4.3 指针的关系运算](#4.3 指针的关系运算)
1. 内存和地址
1.1 内存

在讲内存和地址之前,我们想有个生活中的案例:
假设有一栋宿舍楼,把你放在楼里,楼上有 100 个房间,但是房间没有编号,你的一个朋友来找你玩,如果想找到你,就得挨个房子去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:
1 一楼: 101, 102, 103...
2 二楼: 201, 202, 203...
3 ...
有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你

生活中,每个房间有了房间号,就能提高效率,能快速的找到房间
如果把上面的例子对照到计算机中,又是怎么样呢?
我们知道计算机上 CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存是 8GB/16GB/32GB 等,那这些内存空间如何高效的管理呢?
其实也是把内存划分为一个个的内存单元,每个内存单元的大小取 1 个字节。
计算机中常见的单位(补充):
一个比特位可以存储一个 2 进制的位 1 或者 0



1.2 究竟该如何理解编址


2. 指针变量和地址
2.1 取地址操作符(&)
理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量就是想内存申请空间,比如:




2.2 指针变量和解引用操作符(*)
2.2.1 指针变量
那我们通过取地址操作符 (&) 拿到的地址是一个数值,比如:0x006FFD70,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量中。
比如:
cs
// 引入标准输入输出头文件(后续若要打印内容会用到此头文件)
#include <stdio.h>
// 主函数:程序的执行入口
int main()
{
// 定义整型变量a,并给a初始赋值为10
// 变量a会占用一块内存空间,这块空间有唯一对应的内存地址
int a = 10;
// 定义指针变量pa:
// 1. "int *" 表示pa是一个"指向整型变量的指针变量"(用于存储其他整型变量的内存地址)
// 2. "&a" 是取地址操作符,作用是获取变量a的内存地址
// 整行含义:把变量a的内存地址,存储到指针变量pa中
int * pa = &a;//取出a的地址并存储到指针变量pa中
// 主函数返回0,表示程序正常执行结束
return 0;
}

指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。


2.2.2 解引用操作符
我们将地址保存起来,未来是要使用的,那怎么使用呢?
在现实生活中,我们使用地址要找到一个房间,在房间里可以拿去或者存放物品。
C 语言中其实也是一样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习一个操作符叫解引用操作符 (*)。

cs
// 引入标准输入输出头文件,用于后续可能的打印等输入输出操作
#include <stdio.h>
// 主函数:程序的执行入口
int main()
{
// 定义整型变量a,并给a初始赋值为100
// 变量a会占用一块内存空间,对应唯一的内存地址
int a = 100;
// 定义指向整型的指针变量pa:
// 1. "int*" 表示pa是"指向整型变量的指针"
// 2. "&a" 是取地址操作符,获取变量a的内存地址
// 整行含义:将变量a的内存地址存储到指针变量pa中
int* pa = &a;
// "*pa" 是解引用操作符:通过指针pa中存储的地址,找到它指向的变量(即a)
// 整行含义:通过指针pa,将它指向的变量a的值修改为0
*pa = 0;
// 主函数返回0,表示程序正常执行结束
return 0;
}
上面代码中第 7 行就使用了解引用操作符,*pa 的意思就是通过 pa 中存放的地址,找到指向的空间,*pa 其实就是 a 变量了;所以*pa = 0,这个操作符是把 a 改成了 0.
有同学肯定在想,这里如果目的就是把 a 改成 0 的话,写成 a = 0; 不就完了,为啥非要使用指针呢?
其实这里是把 a 的修改交给了 pa 来操作,这样对 a 的修改,就多了一种的途径,写代码就会更加灵活,后期慢慢就能理解了。
2.3 指针变量的大小

前面的内容我们了解到,32 位机器假设有 32 根地址总线,每根地址线出来的电信号转换成数字信号后是 1 或者 0,那我们把 32 根地址线产生的 2 进制序列当做一个地址,那么一个地址就是 32 个 bit 位,需要 4 个字节才能存储。
如果指针变量是用来存放地址的,那么指针变量的大小就得是 4 个字节的空间才可以。
同理 64 位机器,假设有 64 根地址总线,一个地址就是 64 个二进制位组成的二进制序列,存储起来就需要 8 个字节的空间,指针变量的大小就是 8 个字节。
cs
#include <stdio.h> // 引入标准输入输出头文件,用于printf函数输出
// 代码功能注释:验证不同类型指针变量的大小
// 指针变量的大小由当前平台的地址总线宽度决定
// 32位平台:地址占32个bit(即4字节),所有指针变量大小为4字节
// 64位平台:地址占64个bit(即8字节),所有指针变量大小为8字节
int main() // 主函数,程序执行入口
{
// 打印char类型指针的大小
// char*是指向char类型变量的指针,其大小由平台决定
printf("%zd\n", sizeof(char *));
// 打印short类型指针的大小
printf("%zd\n", sizeof(short *));
// 打印int类型指针的大小
printf("%zd\n", sizeof(int *));
// 打印double类型指针的大小
printf("%zd\n", sizeof(double *));
return 0; // 主函数返回0,程序正常结束
}

结论:
-
32 位平台下地址是 32 个 bit 位,指针变量大小是 4 个字节
-
64 位平台下地址是 64 个 bit 位,指针变量大小是 8 个字节
-
注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
3. 指针变量类型的意义
指针变量的大小和类型无关,只要是指针变量,在同一个平台下,大小都是一样的,为什么还要有各种各样的指针类型呢?
其实指针类型是有特殊意义的,我们接下来继续学习。
3.1 指针的解引用
对比,下面两段代码,主要在调试时观察内存的变化。
cs
// 代码1:演示int类型指针的解引用操作
#include <stdio.h>
int main()
{
// 定义int类型变量n,赋值为十六进制数0x11223344(占4个字节)
int n = 0x11223344;
// 定义指向int类型的指针pi,存储n的地址
int *pi = &n;
// 解引用pi:通过指针访问其指向的int类型变量(占4个字节),将n的4个字节全部赋值为0
*pi = 0;
return 0;
}
cs
// 代码2:演示char类型指针的解引用操作
#include <stdio.h>
int main()
{
// 定义int类型变量n,赋值为十六进制数0x11223344(占4个字节)
int n = 0x11223344;
// 定义指向char类型的指针pc,将n的地址强制转换为char*类型
char *pc = (char *)&n;
// 解引用pc:通过指针访问其指向的char类型变量(仅占1个字节),仅将n的1个字节赋值为0
*pc = 0;
return 0;
}
调试结果:
-
代码 1 会将
n的 4 个字节全部改为 0; -
代码 2 仅将
n的第一个字节改为 0。
结论:
指针的类型决定了解引用时的操作权限(一次能操作的字节数)。
示例:
-
char*类型指针解引用,仅能访问 1 个字节; -
int*类型指针解引用,能访问 4 个字节。
3.2 指针+-整数
先看一段代码,调试观察地址的变化
cs
#include <stdio.h> // 引入标准输入输出头文件,用于printf函数输出
int main()
{
// 定义int类型变量n,赋值为10(n占4个字节内存)
int n = 10;
// 定义char*类型指针pc,将n的地址强制转换为char*类型后赋值
char *pc = (char*)&n;
// 定义int*类型指针pi,存储n的地址
int *pi = &n;
// 打印变量n的内存地址
printf("%p\n", &n);
// 打印char*类型指针pc存储的地址(与&n相同)
printf("%p\n", pc);
// 打印pc自增1后的地址:char*指针+1,步长为1字节
printf("%p\n", pc+1);
// 打印int*类型指针pi存储的地址(与&n相同)
printf("%p\n", pi);
// 打印pi自增1后的地址:int*指针+1,步长为4字节(对应int类型的大小)
printf("%p\n", pi+1);
return 0; // 主函数返回0,程序正常结束
}
代码运行的结构如下:

-
指针运算的现象:
-
char*类型指针变量 + 1,会跳过 1 个字节; -
int*类型指针变量 + 1,会跳过 4 个字节。
-
-
原理说明:
这一差异由指针变量的类型决定;指针 + 1(或 - 1)的本质,是跳过 "1 个指针指向的元素" 对应的内存空间。
-
结论:
指针的类型,决定了指针向前 / 向后移动一步时的偏移距离。
内容解读:
这段内容揭示了指针类型的另一核心作用:控制指针运算的偏移步长 。指针的 "+1""-1" 并非简单的数值加 / 减 1,而是以 "指针指向的元素大小" 为单位偏移 ------ 例如char*对应 1 字节元素,步长为 1;int*对应 4 字节元素,步长为 4。这一规则是 C 语言中遍历连续数据(如数组)的基础:通过指针运算,可精准定位到下一个 / 上一个同类型元素,避免内存访问错位。
3.3 void*指针
在指针类型中有一种特殊的类型是void *类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void*类型的指针不能直接进行指针的 +- 整数和解引用的运算。
举例:
cs
#include <stdio.h> // 引入标准输入输出头文件(此处未实际使用)
int main() // 主函数,程序执行入口
{
int a = 10; // 定义int类型变量a,占用4字节内存,赋值为10
int* pa = &a; // 定义int*类型指针pa,正确接收int类型变量a的地址(类型匹配)
// 【类型不匹配警告】:char*类型指针直接接收int类型变量的地址,C语言规范写法需强制类型转换
char* pc = &a; // 问题行:应改为 char* pc = (char*)&a; 消除类型不匹配警告
return 0; // 主函数返回0,程序正常结束
}
在之前的代码中,将 int 类型变量的地址赋值给 char类型指针变量时,编译机会弹出警告,原因是两种指针类型不兼容;而若使用 void类型指针接收该地址,则不会出现此类问题。

使用void*类型的指针接收地址:
cs
#include <stdio.h> // 引入标准输入输出头文件
int main()
{
int a = 10; // 定义int类型变量a,赋值为10
void* pa = &a; // 定义void*类型指针pa,接收int变量a的地址(void*可接收任意类型地址)
void* pc = &a; // 定义void*类型指针pc,同样接收int变量a的地址
// 【错误】void*类型指针无具体类型,无法直接解引用(无法确定访问的字节数)
*pa = 10;
// 【错误】同理,void*类型指针不能直接进行解引用操作
*pc = 0;
return 0; // 主函数返回0
}
VS编译代码的结果:

这里我们可以看到,void* 类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。
那么 void* 类型的指针到底有什么用呢?
一般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据,在《深入理解指针(5)》中我们会讲解。
4. 指针运算
指针的基本运算有三种,分别是:
· 指针+- 整数
· 指针-指针
· 指针的关系运算
4.1 指针+-整数
因为数组在内存中是连续存放的,只要直到第一个元素的地址,顺藤摸瓜就能找到后面的所有元素




cs
#include <stdio.h> // 引入标准输入输出头文件,支持printf函数的使用
// 代码功能:通过"指针+整数"的运算方式实现数组元素的遍历
int main()
{
// 定义int类型数组arr,初始化包含10个整型元素
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
// 定义int*类型指针p,指向数组的第一个元素arr[0]的地址
int *p = &arr[0];
// 定义循环控制变量i,初始值为0
int i = 0;
// 计算数组的元素个数:数组总字节数 ÷ 单个元素的字节数
int sz = sizeof(arr)/sizeof(arr[0]);
// 循环遍历数组,i的取值范围是0到数组元素个数-1
for(i = 0; i < sz; i++)
{
// p+i:指针p进行"+i"运算,步长为int类型的大小(通常4字节),指向数组的第i个元素
// *(p+i):解引用运算,获取p+i指向位置的元素值,并通过printf打印
printf("%d ", *(p + i));//p+i 这里就是指针+整数的运算形式
}
return 0; // 主函数返回0,标志程序正常结束
}
4.2 指针-指针


cs
// 代码功能:通过"指针-指针"的运算实现字符串长度的计算
#include <stdio.h>
// 自定义函数my_strlen:计算字符串的长度,参数为char*类型的字符串指针
int my_strlen(char *s)
{
char *p = s; // 定义指针p,初始指向字符串s的起始位置
// 循环:当p指向的内容不是字符串结束符'\0'时,继续执行
while(*p != '\0' )
p++; // 指针p自增,依次指向字符串的下一个字符
// 返回p与s的差值:即两个指针之间的字符个数(字符串长度)
return p-s;
}
int main()
{
// 调用my_strlen函数,计算字符串"abc"的长度并打印
printf("%d\n", my_strlen("abc"));
return 0; // 主函数返回0,程序正常结束
}


4.3 指针的关系运算
cs
// 代码功能:通过"指针的关系运算"实现数组遍历
#include <stdio.h>
int main()
{
// 定义int类型数组arr,初始化10个元素
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
// 定义int*类型指针p,指向数组的第一个元素arr[0]的地址
int *p = &arr[0];
// 计算数组的元素个数:数组总字节数 ÷ 单个元素的字节数
int sz = sizeof(arr)/sizeof(arr[0]);
// 指针关系运算:p(当前指向的地址) < arr+sz(数组末尾的下一个地址)
while(p < arr + sz) //指针的大小比较
{
// 解引用指针p,打印当前指向的数组元素
printf("%d ", *p);
// 指针p自增,指向数组的下一个元素
p++;
}
return 0; // 主函数返回0,