【C语言】11. 深入理解指针1
- 一、内存和地址
-
- [1.1 内存(宿舍楼类比)](#1.1 内存(宿舍楼类比))
- [1.2 编址原理](#1.2 编址原理)
- 二、指针变量和地址
-
- [2.1 取地址操作符(&)](#2.1 取地址操作符(&))
- [2.2 指针变量和解引用操作符(*)](#2.2 指针变量和解引用操作符(*))
-
- [(1)指针变量: 存放地址的变量](#(1)指针变量: 存放地址的变量)
- [(2)解引用操作符(*): 通过地址找到指向的对象](#(2)解引用操作符(*): 通过地址找到指向的对象)
- [2.3 指针变量的大小](#2.3 指针变量的大小)
- 三、指针变量类型的意义
-
- [3.1 指针的解引用(权限不同)](#3.1 指针的解引用(权限不同))
- [3.2 指针±整数(步长不同)](#3.2 指针±整数(步长不同))
- [3.3 void* 指针(泛型指针)](#3.3 void* 指针(泛型指针))
- [四、const 修饰指针](#四、const 修饰指针)
-
- [4.1 const 修饰变量](#4.1 const 修饰变量)
- [4.2 const修饰指针变量](#4.2 const修饰指针变量)
- 五、指针运算
-
- [5.1 指针 ± 整数](#5.1 指针 ± 整数)
- [5.2 指针 - 指针](#5.2 指针 - 指针)
- [5.3 指针的关系运算](#5.3 指针的关系运算)
- 六、野指针
-
- [6.1 野指针三大成因](#6.1 野指针三大成因)
- [6.2 规避野指针四步走](#6.2 规避野指针四步走)
- 七、assert断言
-
- [7.1 基本用法](#7.1 基本用法)
- [7.2 特点](#7.2 特点)
- [7.3 开启/关闭](#7.3 开启/关闭)
- 八、指针的使用和传址调用
-
- [8.1 模拟实现 strlen](#8.1 模拟实现 strlen)
- [8.2 传值调用 vs 传址调用(Swap经典案例)](#8.2 传值调用 vs 传址调用(Swap经典案例))
- [8.3 对比总结](#8.3 对比总结)
一、内存和地址
1.1 内存(宿舍楼类比)
我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存是8GB/16GB/32GB等,那这些内存空间如何⾼效的管理呢?
其实也是把内存划分为⼀个个的内存单元,每个内存单元的大小取1个字节。
| 现实世界 | 计算机世界 |
|---|---|
| 宿舍楼 | 内存空间 |
| 房间 | 内存单元(每个1字节) |
| 门牌号 | 地址(编号) |
| 找到房间 | CPU通过地址访问内存 |

其中,每个内存单元,相当于一个学生宿舍,一个字节空间里面能放8个比特位,就好比同学们住的八人间,每个人是一个比特位。
每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到一个内存空间。
生活中我们把门牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。
C语言中给地址起了新的名学叫:指针。

内存单元的编号 = 地址 = 指针
每个内存单元1个字节,里面存放8个比特位。
1.2 编址原理
- CPU通过地址总线发送地址信号到内存
- 32位机器:32根地址线 → 2³²种地址 → 指针占4字节
- 64位机器:64根地址线 → 2⁶⁴种地址 → 指针占8字节
- 硬件层面已完成编址设计,是约定好的共识
二、指针变量和地址
2.1 取地址操作符(&)
c
int a = 10;
&a; // 取出a的地址
printf("%p\n", &a); // %p 打印地址
整型变量a占4个字节,&a取的是首字节地址(较小的那个地址)
知道首地址,就能顺藤摸瓜访问全部4个字节
2.2 指针变量和解引用操作符(*)
(1)指针变量: 存放地址的变量
c
int a = 10;
int *pa = &a; // pa是指针变量,存放a的地址
如何拆解指针类型:
c
int *pa = &a;
// int * → 整体是指针类型
// int → 指向的对象是int类型
// * → 说明pa是指针变量
char类型指针示例:
c
char ch = 'w';
char *pc = &ch; // char* 指向char类型
(2)解引用操作符(*): 通过地址找到指向的对象
c
int a = 100;
int *pa = &a;
*pa = 0; // 等价于 a = 0,通过地址把a改成0
💡 这给了我们修改a的另一种途径,让代码更灵活。
2.3 指针变量的大小

c
printf("%zd\n", sizeof(char *)); // 4(32位)或8(64位)
printf("%zd\n", sizeof(int *)); // 同上
printf("%zd\n", sizeof(double *)); // 同上
⚠️ 结论:指针变量的大小和类型无关,只与系统位数有关;同一平台下所有指针大小都相同。
三、指针变量类型的意义
既然指针大小都一样,为什么还要分不同类型?
3.1 指针的解引用(权限不同)
c
// 代码1:int* 解引用
int n = 0x11223344;
int *pi = &n;
*pi = 0; // 把n的4个字节全部改为0
// 代码2:char* 解引用
int n = 0x11223344;
char *pc = (char*)&n;
*pc = 0; // 只把n的第一个字节改为0

📌 结论:指针的类型决定了,在对指针进行解引用时,一次能访问几个字节。
3.2 指针±整数(步长不同)
c
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", pc); // 假设 0x1000
printf("%p\n", pc+1); // 0x1001(跳1字节)
printf("%p\n", pi); // 假设 0x1000
printf("%p\n", pi+1); // 0x1004(跳4字节)

📌 结论:指针的类型决定了指针向前或向后走一步有多大距离。
3.3 void* 指针(泛型指针)
c
int a = 10;
void *p = &a; // void*:无具体类型的指针,可以接收任意类型的地址
局限性:
❌ 不能直接解引用:*p → 报错
❌ 不能直接±整数:p+1 → 报错
用途: 函数参数中使用,接收不同类型数据的地址,实现泛型编程效果。
四、const 修饰指针
4.1 const 修饰变量
c
const int n = 0;
n = 20; // ❌ 报错!n不能被修改
但可以通过指针绕过限制(不推荐):
c
const int n = 0;
int *p = &n;
*p = 20; // ✅ 能改,但破坏了const的本意
4.2 const修饰指针变量
🎯 速记口诀:const 在 * 左边管内容,在 * 右边管指针本身
c
int *p; // 无限制
const int *p; // const在*左边 → 指向的内容不可变
int *const p; // const在*右边 → 指针本身不可变
const int *const p; // 两边都有 → 内容和指针都不可变
对比表格:

五、指针运算
5.1 指针 ± 整数
利用数组连续存储的特性遍历数组:
c
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
for(int i = 0; i < 10; i++)
{
printf("%d ", *(p + i)); // p+i 就是指针+整数
}
5.2 指针 - 指针
得到两个地址之间相差的元素个数:
c
// 模拟实现strlen
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0')
p++;
return p - s; // 指针相减得到长度
}
⚠️ 要求:两个指针指向同一块连续空间才有意义。
5.3 指针的关系运算
比较地址的高低(大小):
c
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
while(p < arr + sz) // 指针比较
{
printf("%d ", *p);
p++;
}
六、野指针
概念:野指针就是指针指向的位置是未知的、随机的、不正确的。
6.1 野指针三大成因
① 指针未初始化
c
int *p; // 局部变量未初始化,默认随机值
*p = 20; // ❌ 危险!p指向未知位置
② 指针越界访问
c
int arr[10] = {0};
int *p = &arr[0];
for(int i = 0; i <= 11; i++) // 循环12次,越界
{
*(p++) = i; // 最后两次访问越界 → 野指针
}
③ 指针指向的空间被释放
c
int* test()
{
int n = 100;
return &n; // ❌ 返回局部变量地址
}
int main()
{
int *p = test(); // p成为野指针
printf("%d\n", *p); // ❌ 危险!空间已释放
}
6.2 规避野指针四步走

NULL的定义:
c
#define NULL ((void*)0) // 0地址不可读写
正确示例:
c
int *p = NULL; // 不知道指向哪就赋NULL
// ... 使用时
if(p != NULL) // 先检查
{
*p = 10; // 安全使用
}
七、assert断言
7.1 基本用法
c
#include <assert.h>
assert(p != NULL); // 条件为假时终止程序并报错
7.2 特点

7.3 开启/关闭
c
#define NDEBUG // 定义后禁用assert
#include <assert.h>
- Debug版本:启用,帮助排查问题
- Release版本:自动优化掉,不影响效率
八、指针的使用和传址调用
8.1 模拟实现 strlen
c
int my_strlen(const char *str)
{
int count = 0;
assert(str); // 确保指针非空
while(*str) // 等价于 *str != '\0'
{
count++;
str++;
}
return count;
}
8.2 传值调用 vs 传址调用(Swap经典案例)
❌ 传值调用(失败):
c
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp; // 只交换了形参,不影响实参
}
int main()
{
int a = 10, b = 20;
Swap1(a, b); // 传值 → 交换失败
// a=10, b=20(没变)
}
✅ 传址调用(成功):
c
void Swap2(int *px, int *py)
{
int tmp = *px;
*px = *py;
*py = tmp; // 通过地址直接修改main中的a和b
}
int main()
{
int a = 10, b = 20;
Swap2(&a, &b); // 传址 → 交换成功
// a=20, b=10(交换成功!)
}
8.3 对比总结

🎯 终极速记卡(一页总结)
