TIP如果你从未接触过 C 语言, 那么我建议你先阅读前面的文章:
指针
有许多人说, 指针是 C 语言中最难的知识点, 说它抽象、难以理解
也说指针是造成 C 语言不安全的原因之一, 说它危险、不受控制
但也有许多人说, 指针是 C 语言的魅力所在, 说它高效、灵活自由
指针几乎是所有 C 语言使用者心中一个又爱又恨的角色
那么, 究竟什么是指针?
内存 **
要理解 C 语言的指针, 就必须要先了解一个概念: 内存
可能有人会问: 是手机上 8G+128G 12G+256G 16G+512G 配置里, 128G 256G 512G的”内存”吗?
不是, 严格意义上来讲, 手机上的128G 256G等不叫内存, 前面的8G 12G 16G才叫内存
内存, 是电脑上的内存条的内存, 是手机上的运行内存, 通常也被称为 RAM
而手机常见配置里的128G 256G 512G, 以及电脑上的硬盘等, 应该被称为外存
不谈手机, 电脑系统中, 你可以很简单的的看到自己的电脑内存是多大的、现在已经用了多少:
-
Linux终端直接输入
free并回车:
total, 就是总可用内存used, 就是已用内存free, 就是空闲可用内存 -
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;}这段代码的执行结果为:

从结果来看
-
第一次, 尝试打印
*pVal打印成功, 打印出的值为
20即, 通过
*pVal获取到的值与val相同事实上,
*pVal访问到的就是val所占用的内存单元因为
pVal存储的就是val的地址,*pVal访问的当然就是val的内存单元, 其实就可以看作是访问val本身 -
第二次, 尝试给
*pVal赋值30, 并打印*pVal和val从结果看,
*pVal成功赋值为30而且, 打印
*pVal和val的结果都是30事实上, 还是相同的原因:
*pVal访问到的就是val所占用的内存单元, 尝试给*pVal赋值, 就是尝试修改pVal存储地址的内存单元, 这个内存单元就是val所占用的内存单元所以给
*pVal赋值, 就相当于修改val本身
所以, 当一个指针存储有另一个变量的地址时, 就可以直接通过指针操作对应地址的内存单元, 也就能够操作这个变量的值
为了更形象的理解, 我们也可以将 指针存储变量的地址 理解为 指针指向这个变量的地址:

我们可以通过指针, 访问此指针指向的地址
了解了指针本身, 再来了解一些其他的 指针基本用法:
-
多个指针可以指向同一个地址
这个意思就是说, 同一个变量的地址, 可以赋值给多个不同的指针, 且这多个指针都可以访问这个变量的地址
int val = 20;int* pVal1 = &val;int* pVal2 = &val;int* pVal3 = &val;int* pVal4 = &val;int* pVal5 = &val;此代码定义 5 个指针, 赋值同一个变量的地址
可以理解为: 5 个指针指向同一个地址
这也意味着, 只要
val的值被修改, 通过*访问 5 个指针指向地址的值, 都会一起变动 -
指针的指向是可以发生改变的
这个意思是说, 一个指针, 可以先指向变量 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
如果你从未接触过指针, 很有可能会混淆两个操作: 给指针赋值 和 给指针指向的地址赋值
这里我单独区分一下:
-
给指针赋值
是修改指针变量本身的值, 即 修改指针变量所存储的地址, 即 改变指针的指向
int val1;int val2;int* pVal;pVal = &val1; // 给 pVal 本身赋值pVal = &val2; // 给 pVal 本身赋值
-
给指针指向的地址赋值
是修改指针所指向地址的内存空间的值
int val1 = 20;int* pVal = &val1;*pVal = 30; // 先 * 解引用, 访问到指向的地址, 再给此地址的内存空间赋值
-
-
指针类型最好与 要指向地址的变量数据类型相匹配
这个原因暂时不做分析
指针变量的大小
C 语言中, 变量类型决定了变量的大小: char占1字节, int占4字节, long long占8字节等
而指针的类型是数据类型*, 那么, 指针变量的大小是多少呢?
指针变量的大小, 其实与定义指针时使用的数据类型无关
指针变量的大小, 在通用计算机平台, 通常只与操作系统有关
指针是用来存储地址的, 而地址 是系统分配的内存单元的编号, 即 指针本质上存储的是内存单元的编号
那么指针的大小, 其实就是这个系统分配的内存单元的编号的大小, 所以指针的大小与操作系统有关
更具体地说, 指针的大小其实与操作系统的位数有关
在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指针的大小其实也说明了:
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的内存
64位系统也是相同的思路也是由于
32位硬件存在限制, 所以才有了64位的平台
64位也存在限制, 但几乎无法被触碰到
64位的上限是:18,446,744,073,709,551,615 + 1字节, 换算一下:17,179,869,184 GB