3418 字
17 分钟
从零接触C语言(初览)-XI: 指针
TIP

如果你从未接触过 C 语言, 那么我建议你先阅读前面的文章:

📌 从零开始接触 C 语言

指针#

有许多人说, 指针是 C 语言中最难的知识点, 说它抽象、难以理解

也说指针是造成 C 语言不安全的原因之一, 说它危险、不受控制

但也有许多人说, 指针是 C 语言的魅力所在, 说它高效、灵活自由

指针几乎是所有 C 语言使用者心中一个又爱又恨的角色

那么, 究竟什么是指针?

内存 **#

要理解 C 语言的指针, 就必须要先了解一个概念: 内存

可能有人会问: 是手机上 8G+128G 12G+256G 16G+512G 配置里, 128G 256G 512G的”内存”吗?

不是, 严格意义上来讲, 手机上的128G 256G等不叫内存, 前面的8G 12G 16G才叫内存

内存, 是电脑上的内存条的内存, 是手机上的运行内存, 通常也被称为 RAM

而手机常见配置里的128G 256G 512G, 以及电脑上的硬盘等, 应该被称为外存

不谈手机, 电脑系统中, 你可以很简单的的看到自己的电脑内存是多大的、现在已经用了多少:

  1. Linux

    终端直接输入free并回车:

    total, 就是总可用内存

    used, 就是已用内存

    free, 就是空闲可用内存

  2. Windows

    可以直接打开任务管理器查看当前的内存使用状态:

当然, 这些不是重点

为什么理解 C 语言的指针, 要先理解内存呢?

因为, 在现代通用计算机中, 所有的 C 语言代码编译为程序之后, 在运行时, 都是运行在内存上的

而指针, 则是 C 语言提供的一种可以直接操作内存的机制

NOTE

这里涉及到的内存, 均不是直接指物理内存, 而是指由操作系统映射、管理的内存, 暂时不需要过于关注

但要注意到: 如果不涉及到操作系统内核的开发, 内存不是任何语言所拥有的东西, 所有语言能够访问、操作的内存任何时候都是操作系统层面的

类似什么 C 语言的内存模型、C++的内存模型等, 实际上说的都是对应语言程序运行之后, 操作系统对程序分配的内存模型

总结一句话, 在不涉及操作系统内核开发的前提下, 内存永远是操作系统的, 而不属于任何语言, 语言也不拥有内存模型

当一个程序需要运行的时候, 为了程序能够正常的运行, 操作系统就要给程序分配一块内存, 让程序的代码、数据能够存放在内存中, 然后程序才能运行起来

一般情况下, 内存是由操作系统分配管理的

为了有效地使用内存, 操作系统会将内存划分为一块一块的内存单元, 每个内存单元是 1 个字节

为了更好地管理、有效地访问内存, 操作系统还会对每个内存单元进行唯一编号, 内存单元的编号就是我们常说的 内存单元的地址, 即 内存地址

简单用图片理解, 内存可以看作是这样的:

什么是指针? **#

当程序被加载运行后, 它的代码和数据都会被加载到内存中

换句话说, 操作系统会为程序分配若干内存单元, 并把程序的”代码”和”数据”存放到里面

所以, 以 C 语言程序的角度来看, 我们在代码中定义的各种变量, 也会被加载到内存中

且不同数据类型占用内存的大小是不同的: char类型变量只需要占用 1 字节(即 1 个内存单元), int类型的变量需要占用连续的 4 字节(即 4 个连续的内存单元)

上面介绍到, 操作系统会对每一个内存单元都进行编号, 这个编号就是内存单元的地址

而, C 语言规定 以变量所占用的第一个内存单元的地址作为该变量的地址, 且每个变量都有属于自己的地址

指针, 就是 C 语言提供的一种能够存储并操作这些地址的机制

指针, 可以存储地址并访问地址, 这种能力看起来和变量很相似

事实上, 指针本质就是一种变量, 不过与普通变量不同的是, 指针存储的是地址这种特殊数据, 而普通变量存储的是特定类型的数据

变量的地址#

在正式开始使用指针之前, 我们先来看一下变量是否有自己的地址

其实很简单, 在 介绍操作符的文章中 有一个操作符是&取地址, 我们可以用这个操作符取变量的地址, 并打印显示:

#include <stdio.h>
int main() {
int val = 10;
printf("val addr: %p\n", &val);
return 0;
}

这段代码编译运行的结果是:

从结果看到, val确实存在地址0x7fffe135589c

那么, 变量val在内存中的示意图暂时可以简单理解为:

指针的定义#

指针是一种变量, 但它不是普通的变量, 它与普通变量的定义也有一些差别

介绍操作符的文章中 有一个操作符是*, 它可以用来定义指针

数据类型* 标识符;

没错, 定义指针变量与定义普通变量只有一个区别, 那就是*

定义变量时, 在数据类型之后加上*, 定义的就是指针变量

char* pS = NULL;
int* pI = NULL;
long* pL = NULL;

向上面这样, 就定义了三种类型的指针变量: char* int* long*, 且均赋值为NULL

指针变量的类型, 就分别对应char* int* long*

NOTE

NULL是 C 语言标准提供的一个宏定义, 在 C 语言中表示空地址、空指针

在 C 语言中, 其实就是 0强制转换为空指针

#define NULL ((void*)0)

当然, 除了给指针赋值为NULL之外, 我们还可以使用取地址符&, 取出已有变量的地址, 存储到指针变量中

需要注意的是, 普通变量和指针变量的类型要一致, 具体原因不在本篇说明

#include <stdio.h>
int main() {
int val = 20;
int* pVal = &val;
printf("val: %d, addr: %p\npVal: %p\n", val, &val, pVal);
return 0;
}

这段代码, 定义int类型变量val, 取变量val的地址 并赋值给 定义的int类型指针pVal

并依次打印: val的值, val的地址, pVal的值

代码的编译运行结果为:

从结果看到: &val取到的地址是0x7ffc810495e4, 而pVal的值也是0x7ffc810495e4

即, pVal这个指针变量, 存储了变量val的地址

指针的使用#

已经了解了指针的定义和指针的赋值, 但这有什么用呢?

我们知道, 变量存储数据, 是为了在之后的代码中, 通过这个变量使用这个数据

指针也是一样的, 指针存储地址, 是为了之后可以通过指针访问、操作这个地址的内存单元

那么指针是如何访问地址的内存单元的呢?

答案, 在上面也已经提到过了: *解引用操作符

*不仅在定义指针时需要, 在访问指针存储地址的内存单元时也需要

使用方法也很简单, 在指针变量前加*就可以访问指针存储地址的内存单元:

#include <stdio.h>
int main() {
int val = 20;
int* pVal = &val;
printf("pVal 指向地址的数据值为: %d\n", *pVal);
printf("val 的值为: %d\n", val);
*pVal = 30;
printf("pVal 指向地址的数据值为: %d\n", *pVal);
printf("val 的值为: %d\n", val);
return 0;
}

这段代码的执行结果为:

从结果来看

  1. 第一次, 尝试打印*pVal

    打印成功, 打印出的值为20

    即, 通过*pVal获取到的值与val相同

    事实上, *pVal访问到的就是val所占用的内存单元

    因为pVal存储的就是val的地址, *pVal访问的当然就是val的内存单元, 其实就可以看作是访问val本身

  2. 第二次, 尝试给*pVal赋值30, 并打印*pValval

    从结果看, *pVal成功赋值为30

    而且, 打印*pValval的结果都是30

    事实上, 还是相同的原因: *pVal访问到的就是val所占用的内存单元, 尝试给*pVal赋值, 就是尝试修改pVal存储地址的内存单元, 这个内存单元就是val所占用的内存单元

    所以*pVal赋值, 就相当于修改val本身

所以, 当一个指针存储有另一个变量的地址时, 就可以直接通过指针操作对应地址的内存单元, 也就能够操作这个变量的值

为了更形象的理解, 我们也可以将 指针存储变量的地址 理解为 指针指向这个变量的地址:

我们可以通过指针, 访问此指针指向的地址


了解了指针本身, 再来了解一些其他的 指针基本用法:

  1. 多个指针可以指向同一个地址

    这个意思就是说, 同一个变量的地址, 可以赋值给多个不同的指针, 且这多个指针都可以访问这个变量的地址

    int val = 20;
    int* pVal1 = &val;
    int* pVal2 = &val;
    int* pVal3 = &val;
    int* pVal4 = &val;
    int* pVal5 = &val;

    此代码定义 5 个指针, 赋值同一个变量的地址

    可以理解为: 5 个指针指向同一个地址

    这也意味着, 只要val的值被修改, 通过*访问 5 个指针指向地址的值, 都会一起变动

  2. 指针的指向是可以发生改变的

    这个意思是说, 一个指针, 可以先指向变量 1 的地址, 然后再指向变量 2 的地址或其他变量的地址

    但, 一个指针的指向 同时最多只有一个, 即 指针不能在指向变量 1 地址的同时 还指向变量 2 的地址

    #include <stdio.h>
    int main() {
    int val1 = 20;
    int val2 = 30;
    int* pVal = &val1;
    // 此时, pVal指向val1的地址
    printf("pVal: %p, *pVal: %d\n", pVal, *pVal);
    pVal = &val2; // 给pVal赋值, 而不是给*pVal赋值
    // 此时, pVal指向val2的地址
    printf("pVal: %p, *pVal: %d\n", pVal, *pVal);
    return 0;
    }

    NOTE

    如果你从未接触过指针, 很有可能会混淆两个操作: 给指针赋值给指针指向的地址赋值

    这里我单独区分一下:

    1. 给指针赋值

      是修改指针变量本身的值, 即 修改指针变量所存储的地址, 即 改变指针的指向

      int val1;
      int val2;
      int* pVal;
      pVal = &val1; // 给 pVal 本身赋值
      pVal = &val2; // 给 pVal 本身赋值

    2. 给指针指向的地址赋值

      是修改指针所指向地址的内存空间的值

      int val1 = 20;
      int* pVal = &val1;
      *pVal = 30; // 先 * 解引用, 访问到指向的地址, 再给此地址的内存空间赋值

  3. 指针类型最好与 要指向地址的变量数据类型相匹配

    这个原因暂时不做分析

指针变量的大小#

C 语言中, 变量类型决定了变量的大小: char1字节, int4字节, long long8字节等

而指针的类型是数据类型*, 那么, 指针变量的大小是多少呢?

指针变量的大小, 其实与定义指针时使用的数据类型无关

指针变量的大小, 在通用计算机平台, 通常只与操作系统有关

指针是用来存储地址的, 而地址 是系统分配的内存单元的编号, 即 指针本质上存储的是内存单元的编号

那么指针的大小, 其实就是这个系统分配的内存单元的编号的大小, 所以指针的大小与操作系统有关

更具体地说, 指针的大小其实与操作系统的位数有关

32位系统中, 指针的大小为4字节

64为系统中, 指针的大小通常为8字节

同样可以用代码举例:

#include <stdio.h>
int main() {
printf("char* size: %lu\n", sizeof(char*));
printf("short* size: %lu\n", sizeof(short*));
printf("int* size: %lu\n", sizeof(int*));
printf("double* size: %lu\n", sizeof(double*));
return 0;
}

现在的操作系统, 基本都用的64位版本了, 所以打印出来大概率是8

NOTE

指针的大小其实也说明了:

  1. 32位系统为内存单元进行编号, 最大的编号也只占用4字节, 也就是32

    32位无符号整数的范围是: [0(32位0), 4,294,967,295(32位1)]

    即, 系统为内存单元编号, 最小是0(0x00000000), 最大是4,294,967,295(0xFFFFFFFF)

    每个内存单元是1字节, 一共就是4294967295 + 1个字节, 换算一下也就是4GB

    事实上, 由于 CPU 硬件限制, 基于此限制开发的32位系统最多也就只能使用4GB的内存, 进而就只能管理4GB的内存

  2. 64位系统也是相同的思路

    也是由于32位硬件存在限制, 所以才有了64位的平台

    64位也存在限制, 但几乎无法被触碰到

    64位的上限是: 18,446,744,073,709,551,615 + 1字节, 换算一下: 17,179,869,184 GB

作者
Humid1ch
发布于
2025-09-03
许可协议
CC BY-NC-SA 4.0