文章目录
-
-
-
- **一、内存和地址:从生活到计算机的类比**
- **二、指针变量和地址:把地址存起来**
- **三、取地址操作符(&)**
- **四、指针变量**
- **五、解引用操作符(*)**
- **六、指针类型的意义**
- [**七、void* 指针**](#七、void 指针*)
- [**八、void* 指针的用途**](#八、void 指针的用途*)
- **内容总结**
-
- ASCII图演示:C语言指针原理
-
- [1. 内存单元与地址(宿舍楼类比)](#1. 内存单元与地址(宿舍楼类比))
- [2. CPU与内存数据传输](#2. CPU与内存数据传输)
- [3. 变量在内存中的存储](#3. 变量在内存中的存储)
- [4. 指针变量的工作原理](#4. 指针变量的工作原理)
- [5. 不同类型指针的解引用](#5. 不同类型指针的解引用)
- [6. 指针加减整数的步长](#6. 指针加减整数的步长)
- [7. 数组与指针遍历](#7. 数组与指针遍历)
- [8. void* 指针示意图](#8. void* 指针示意图)
- [9. 指针变量大小对比](#9. 指针变量大小对比)
- [10. 完整的示例流程](#10. 完整的示例流程)
-
一、内存和地址:从生活到计算机的类比
想象你住在一栋宿舍楼里,每个房间都有唯一的房间号。朋友来找你,只需知道房间号就能快速定位。
在计算机世界里,内存就是这栋宿舍楼 。内存被划分成无数个小格子,每个格子是一个内存单元 (相当于房间),而每个内存单元都有一个唯一的编号,这就是内存单元的地址(相当于房间号)。数据就存放在这些有编号的单元里。当CPU(中央处理器)需要处理数据时,它通过地址这个"房间号"找到对应的内存单元,读取或写入数据。
1. 硬件视角:CPU如何与内存通信?
CPU和内存是计算机中两个独立的硬件,它们通过一组叫做"总线"的线路连接。其中,地址总线负责传输地址信息。
- 读数据 :CPU通过控制总线发送"读"指令,同时通过地址总线将目标地址发送给内存。内存接到指令后,从指定地址取出数据,再通过数据总线传回CPU。
- 写数据:CPU通过控制总线发送"写"指令,并通过地址总线发送目标地址,然后将要写入的数据通过数据总线传给内存,内存将其存入指定位置。
内存单元的地址(如 0x00000001)并非人为在软件中命名,而是由硬件设计决定的,就像钢琴键上虽然没标音符名,但演奏者都知道哪个键是哪个音,这是一种硬件层面的约定。
2. 内存单位与编址
为了方便管理,计算机将内存划分为一个个单元,每个单元的大小固定为 1字节 (Byte)。1字节包含8个比特位 (bit),每个比特位可以存储一个二进制数(0或1)。
常见的存储单位换算关系如下:
- 1 Byte = 8 bit
- 1 KB = 1024 (2^10) Byte
- 1 MB = 1024 KB
- 1 GB = 1024 MB
- ... 以此类推,TB、PB等。
3. 核心概念:地址 == 指针
在C语言中,我们给内存单元的编号起了个别名,就叫地址 。同时,C语言中还有一个非常重要的概念------指针 。其实,内存单元的编号(地址)就是指针。我们可以这样理解:
内存单元的编号 = 地址 = 指针
二、指针变量和地址:把地址存起来
在C语言中创建一个变量(如 int a = 10;),就是向内存申请了一块空间。这块空间会占用一个或多个内存单元(int 类型通常占4个字节)。
如何找到这块空间?我们需要它的地址。变量a的地址,就是它所占用的多个字节中,地址值最小的那个字节的地址,我们称之为首地址。
通过调试工具(如VS中的内存窗口),我们可以直观地看到变量a从首地址开始,连续占用4个字节的内存单元,里面存放了数字10(通常以十六进制显示,如 0a 00 00 00)。
三、取地址操作符(&)
C语言提供了取地址操作符 & 来获取变量的首地址。
c
int a = 10;
&a; // 这行代码会取出变量a的首地址
printf("%p\n", &a); // 用%p格式打印地址
只要拿到了这个首地址,就等于掌握了访问整个变量a的钥匙,因为我们可以根据变量类型(int)知道它后续还占用了几个字节。
四、指针变量
我们得到的地址可以存放到一个专门的变量中,这种用来存放地址的变量,就叫指针变量。
c
int a = 10;
int* pa = &a; // 创建一个指针变量pa,并把a的地址存进去
1. 如何理解指针类型?
对于 int* pa; 这个声明:
*说明了pa是一个指针变量。int说明了pa指向的是一个int类型的变量。也就是说,通过pa这个地址,我们预期能找到并操作一个整数大小的数据。
2. 指针变量的大小
一个有趣的问题是:指针变量本身占多大内存?
答案是:指针变量的大小与它指向的数据类型无关,只取决于程序运行的平台环境(编译器)。
- 在32位(x86)平台下,地址总线是32根,地址用32个比特位表示,所以指针变量大小是 4字节。
- 在64位(x64)平台下,指针变量大小是 8字节 。
无论它是int*、char*还是double*,在同一个平台下,它们的大小都一样。
五、解引用操作符(*)
有了存着地址的指针变量,我们如何通过它来找到并操作它指向的那个变量呢?这就需要用到解引用操作符 *。
c
int a = 10;
int* pa = &a;
*pa = 20; // *pa:通过pa中存放的地址,找到它指向的那个变量(也就是a),然后将其值改为20
printf("%d", a); // 输出 20
这里的 *pa 就完全等价于变量 a 本身。你可以通过 a 直接修改它,也可以通过 *pa 这个"借来的刀"间接修改它,这让代码在特定场景下更加灵活。
六、指针类型的意义
既然指针变量的大小都一样,为什么还要区分 int* 和 char* 呢?这就引出了指针类型的两个核心作用。
1. 决定解引用时的访问权限(一次走几步)
int*的解引用 :因为指针类型是int*,解引用时,编译器就知道要操作的对象是一个整数,应该访问连续的 4个字节。char*的解引用 :因为指针类型是char*,解引用时,编译器就知道要操作的对象是一个字符,应该只访问 1个字节。
2. 决定指针加减整数时的步长(一步跨多远)
int*的加减 :pa + 1表示地址向后移动sizeof(int)个字节,即 4个字节。这在遍历整型数组时非常有用。char*的加减 :cp + 1表示地址向后移动sizeof(char)个字节,即 1个字节。
总结:指针类型决定了指针的"视野"和"步幅"。
七、void 指针*
void* 是一种特殊的指针类型,被称为无具体类型指针 或泛型指针。它可以用来存放任意类型变量的地址。
- 优点:非常包容,能接收任何类型的地址,解决了不同类型指针赋值时的类型不兼容警告。
- 缺点 :因为不知道它具体指向什么类型的数据,编译器无法确定其"视野"和"步幅"。所以,不能对
void*类型的指针进行解引用操作,也不能对其进行加减整数运算。
八、void 指针的用途*
void* 的主要应用场景是在函数参数 中。当我们希望一个函数能够处理多种不同类型的数据时(例如一个通用的内存拷贝函数),就可以将参数设计为 void* 类型。函数内部收到地址后,再根据实际需求将其转换为具体的指针类型来使用。这为实现泛型编程提供了基础。
内容总结
- 指针的本质:指针就是内存单元的编号,也就是地址。它是CPU访问内存数据的唯一方式。
- 指针变量:是专门用来存放地址的变量。
- 核心操作符 :
&:取地址操作符,获取变量的首地址。*:解引用操作符,通过地址找到其指向的变量。
- 指针类型的两大意义 :
- 解引用权限 :决定了一次能访问多少个字节(
int*访问4字节,char*访问1字节)。 - 加减整数步长 :决定了指针向前或向后移动一步跨越多少个字节(
int*+1移动4字节,char*+1移动1字节)。
- 解引用权限 :决定了一次能访问多少个字节(
- 指针变量的大小:只与编译平台有关(32位下4字节,64位下8字节),与指针类型无关。
void*指针 :- 是一种泛型指针,可以存放任何类型的地址。
- 不能进行解引用和加减整数的操作。
- 主要用途是实现泛型编程,作为函数参数来接收不同类型的数据。
ASCII图演示:C语言指针原理
1. 内存单元与地址(宿舍楼类比)
┌─────────────────────────────────────────┐
│ 内存(宿舍楼) │
├─────────────────────────────────────────┤
│ 地址:0x0001 ┌─────────────┐ │
│ │ 数据: 10 │ 房间1 │
│ └─────────────┘ │
│ 地址:0x0002 ┌─────────────┐ │
│ │ 数据: 20 │ 房间2 │
│ └─────────────┘ │
│ 地址:0x0003 ┌─────────────┐ │
│ │ 数据: 30 │ 房间3 │
│ └─────────────┘ │
└─────────────────────────────────────────┘
2. CPU与内存数据传输
地址总线 (32根线)
<──────────────────────────>
┌───┐ ┌────┐
│ │─── 读/写命令 ──────▶│ │
│CPU│ │内存│
│ │◀─── 数据 ───────────│ │
└───┘ (数据总线) └────┘
地址总线工作原理:
32根线,每根0/1 → 2^32种组合
例如:0000...0001 = 地址1
0000...0010 = 地址2
3. 变量在内存中的存储
int a = 10; 的存储示意图:
地址递增 ──▶
┌──────────┬──────────┬──────────┬──────────┐
│ Byte1 │ Byte2 │ Byte3 │ Byte4 │
│ 0x006A │ 0x006B │ 0x006C │ 0x006D │
├──────────┼──────────┼──────────┼──────────┤
│ 0x0A │ 0x00 │ 0x00 │ 0x00 │ ← 10的十六进制(小端序)
│ [00001010]│ [00000000]│ [00000000]│ [00000000]│ ← 二进制
└──────────┴──────────┴──────────┴──────────┘
↑
&a (首地址)
4. 指针变量的工作原理
int a = 10;
int* pa = &a;
内存布局:
变量a: 指针变量pa:
┌────────────────────┐ ┌────────────────────┐
│ 值: 10 │ │ 值: 0x006A │
│ 地址: 0x006A │ │ 地址: 0x1000 │
└────────────────────┘ └────────────────────┘
↑ ↑
└──────────────────────────┘
pa指向a (存放a的地址)
解引用操作 *pa = 20:
┌─────────────────────────────────────────┐
│ 步骤1: 从pa读取地址 0x006A │
│ ↓ │
│ 步骤2: 到地址 0x006A 找到变量a │
│ ↓ │
│ 步骤3: 将a的值修改为20 │
└─────────────────────────────────────────┘
5. 不同类型指针的解引用
int n = 0x11223344;
内存布局 (小端序):
地址: 0x006A 0x006B 0x006C 0x006D
┌──────────┬──────────┬──────────┬──────────┐
│ 0x44 │ 0x33 │ 0x22 │ 0x11 │
└──────────┴──────────┴──────────┴──────────┘
int* pa = &n;
*pa = 0; 的结果:
┌──────────┬──────────┬──────────┬──────────┐
│ 0x00 │ 0x00 │ 0x00 │ 0x00 │ ← 全部清0
└──────────┴──────────┴──────────┴──────────┘
↑pa访问4字节
char* cp = &n;
*cp = 0; 的结果:
┌──────────┬──────────┬──────────┬──────────┐
│ 0x00 │ 0x33 │ 0x22 │ 0x11 │ ← 只改第1字节
└──────────┴──────────┴──────────┴──────────┘
↑cp访问1字节
6. 指针加减整数的步长
int a = 20;
int* pa = &a;
char* cp = &a;
指针位置示意图:
地址: 0x006A 0x006B 0x006C 0x006D
┌────┬────┬────┬────┐
│ │ │ │ │
└────┴────┴────┴────┘
↑
pa, cp (都指向0x006A)
执行 +1 操作后:
int* pa+1: char* cp+1:
┌────┬────┬────┬────┐ ┌────┬────┬────┬────┐
│ │ │ │ │ │ │ │ │ │
└────┴────┴────┴────┘ └────┴────┴────┴────┘
↑ ↑ ↑ ↑
pa pa+1 cp cp+1
(移动4字节) (移动1字节)
7. 数组与指针遍历
int arr[3] = {10, 20, 30};
int* p = arr; // p指向arr[0]
内存布局:
地址: 0x006A─0x006D 0x006E─0x0071 0x0072─0x0075
┌────────────┐ ┌────────────┐ ┌────────────┐
│ 10 │ │ 20 │ │ 30 │
└────────────┘ └────────────┘ └────────────┘
↑ ↑ ↑
p p+1 p+2
arr[0] arr[1] arr[2]
遍历过程:
第1次: *p = 10 → p++ → p指向0x006E
第2次: *p = 20 → p++ → p指向0x0072
第3次: *p = 30 → p++ → p指向0x0076(越界)
8. void* 指针示意图
void* 可以指向任何类型:
int n = 100; char c = 'A'; double d = 3.14;
┌────────────┐ ┌──────┐ ┌────────────────────┐
│ 100 │ │ 'A' │ │ 3.14 │
│ 0x006A │ │0x1000│ │ 0x2000 │
└────────────┘ └──────┘ └────────────────────┘
↑ ↑ ↑
└───────────────────┼─────────────────────────┘
↓
void* ptr = &n;
ptr = &c; // 可以重新指向char
ptr = &d; // 可以重新指向double
但不能: *ptr // 错误!不知道要访问几个字节
也不能: ptr + 1 // 错误!不知道步长是多少
9. 指针变量大小对比
32位系统 (x86):
┌─────────────────────────────────────┐
│ int* ──▶ [4字节] 存放地址 │
│ char* ──▶ [4字节] 存放地址 │
│ double*─▶ [4字节] 存放地址 │
│ void* ──▶ [4字节] 存放地址 │
└─────────────────────────────────────┘
64位系统 (x64):
┌─────────────────────────────────────┐
│ int* ──▶ [8字节] 存放地址 │
│ char* ──▶ [8字节] 存放地址 │
│ double*─▶ [8字节] 存放地址 │
│ void* ──▶ [8字节] 存放地址 │
└─────────────────────────────────────┘
10. 完整的示例流程
c
int a = 10;
int* pa = &a;
*pa = 20;
// 图解整个过程:
//
// 步骤1: 创建变量a
// [a] 地址: 0x006A 值: 10
//
// 步骤2: 创建指针pa
// [pa] 地址: 0x1000 值: 0x006A (存放a的地址)
// │
// └───→ 指向 [a] 0x006A:10
//
// 步骤3: 执行 *pa = 20
// 从pa读取地址 0x006A
// 找到地址0x006A处的变量a
// 将值改为20
//
// 最终结果:
// [pa] 0x1000: 0x006A ──→ [a] 0x006A: 20
这些ASCII图展示了指针的核心概念:地址是门牌号,指针变量是存门牌号的小纸条,解引用就是按门牌号找到房间。希望这些图示能帮助你更直观地理解指针!