如果你从未接触过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语言提供的一种可以直接操作内存的机制
这里涉及到的内存, 均不是直接指物理内存, 而是指由操作系统映射、管理的内存, 暂时不需要过于关注
但要注意到: 如果不涉及到操作系统内核的开发, 内存不是任何语言所拥有的东西, 所有语言能够访问、操作的内存任何时候都是操作系统层面的
类似什么 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*
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; }
如果你从未接触过指针, 很有可能会混淆两个操作: 给指针赋值 和 给指针指向的地址赋值
这里我单独区分一下:
给指针赋值
是修改指针变量本身的值, 即 修改指针变量所存储的地址, 即 改变指针的指向
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
指针的大小其实也说明了:
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