指针入门一

文章目录


一、内存和地址:从生活到计算机的类比

想象你住在一栋宿舍楼里,每个房间都有唯一的房间号。朋友来找你,只需知道房间号就能快速定位。

在计算机世界里,内存就是这栋宿舍楼 。内存被划分成无数个小格子,每个格子是一个内存单元 (相当于房间),而每个内存单元都有一个唯一的编号,这就是内存单元的地址(相当于房间号)。数据就存放在这些有编号的单元里。当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* 类型。函数内部收到地址后,再根据实际需求将其转换为具体的指针类型来使用。这为实现泛型编程提供了基础。


内容总结

  1. 指针的本质:指针就是内存单元的编号,也就是地址。它是CPU访问内存数据的唯一方式。
  2. 指针变量:是专门用来存放地址的变量。
  3. 核心操作符
    • &:取地址操作符,获取变量的首地址。
    • *:解引用操作符,通过地址找到其指向的变量。
  4. 指针类型的两大意义
    • 解引用权限 :决定了一次能访问多少个字节(int* 访问4字节,char* 访问1字节)。
    • 加减整数步长 :决定了指针向前或向后移动一步跨越多少个字节(int*+1 移动4字节,char*+1 移动1字节)。
  5. 指针变量的大小:只与编译平台有关(32位下4字节,64位下8字节),与指针类型无关。
  6. 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图展示了指针的核心概念:地址是门牌号,指针变量是存门牌号的小纸条,解引用就是按门牌号找到房间。希望这些图示能帮助你更直观地理解指针!

相关推荐
KoiHeng1 小时前
网络原理相关内容(三)
网络
时艰.1 小时前
订单系统读写分离方案设计与实现
java
014-code2 小时前
MySQL 事务隔离级别
java·数据库·mysql
hrhcode2 小时前
【Netty】三.ChannelPipeline与ChannelHandler责任链深度解析
java·后端·spring·springboot·netty
摸鱼仙人~2 小时前
主流前端「语言/技术 → 主流框架 → 组件库生态 → 适用场景」解析
前端
程序员Sunday2 小时前
2026 春晚魔术大揭秘:作为程序员,分分钟复刻一个(附源码)
前端
invicinble2 小时前
关于学习技术栈的思考
java·开发语言·学习
切糕师学AI3 小时前
NAT (Network Address Translation,网络地址转换)
运维·服务器·网络
json{shen:"jing"}3 小时前
分割回文串-暴力法
java·算法