目录
- 一:内存
- 二:计算机常见单位
- 三:内存单元
- 四:指针变量和地址
- 五:指针运算
- 六:传值调用与传址调用
- 七:二级指针
- 八:总结
引言
本文面向刚接触 C 语言的初学者,聚焦一个常见困惑:变量在内存中究竟如何存放,指针又为何能"指向"数据。文章从内存与地址的基本概念讲起,逐步拆解指针变量、指针类型、指针运算、传值/传址调用与二级指针,帮你建立一套可落地的指针认知框架,读懂并写出正确的指针代码。
TL;DR
- 内存被划分为以字节为单位的单元,每个单元有唯一编号,这个编号就是地址,也就是指针。
- 指针变量是专门存放地址的变量,int* pa = &a; 中 * 表示它是指针,int 表示它指向的数据类型。
- 指针类型决定解引用的权限(一次操作几个字节)和
+/-整数时的步长。 - 指针大小与类型无关,只与平台有关:x86 为 4 字节,x64 为 8 字节。
- 想在函数内部修改外部变量,必须用传址调用(传地址),这是指针最实用的场景之一。
一:内存
计算机上CPU(中央处理器)在处理数据时,需要的数据是在内存中读取的,处理后的数据也会放回内存中
- 高效内存管理:
- 把内存划分为一个个内存单元,每个内存单元的大小取1个字节,然后给每一个内存单元一个编号(类似于房间编号)
二:计算机常见单位
一个比特位可以存储一个二进制的位1或位0
| bit(比特位) | 计算机中的最小的数据/信息单位 |
|---|---|
| Byte(字节) | 1 Byte = 8 bit |
| KB | 1 KB = 1024Byte |
| MB | 1 MB = 1024 KB |
| GB | 1 GB = 1024 MB |
| TB | 1 TB = 1024 GB |
| PB | 1 PB = 1024 TB |
1024 = 2^10
三:内存单元
- 计算机中,把内存分为若干个有1个字节大小的内存单元
- 每个内存单元相当于每个学生宿舍
- 一个内存单元 = 1 Byte = 8 bit = 8人间
- 一个bit = 一个室友
- 每个内存单元都有一个编号 = 宿舍门牌号
- 有了这个内存单元的编号,CPU就可以快速找到一个内存空间
- 生活中我们把门牌号称为地址;在计算机内存中把内存单元的编号也称为地址
- 内存单元的编号 = 地址 = 指针
- 补充知识:
- 实际上我们编写的运行在x86架构下的计算机上的C语言程序,不会直接访问物理内存
- 程序运行时为变量分配的地址,在程序调试过程中,我们看到的地址都是虚拟地址;虚拟地址不是物理地址,虚拟地址是虚拟地址空间中的地址
- 虚拟地址空间:操作系统(eg:Windows地址)为每个程序提供的一个假想的,独占的,连续的内存空间
- 程序中的虚拟地址,会由CPU的内存管理单元(MMU)映射到物理内存中,其实程序最终还是会访问物理内存的,这个由虚拟地址换成物理地址的过程地址转换/地址映射
- 能直接访问物理内存的情况(非常特殊的领域):
- 1.嵌入式系统:没有MMU(叫单片机或MCU,如ARM Cortex—M系列,Arduino),C程序直接使用物理地址,此时,编译器和程序员需要亲自管理整个内存布局
- 2.操作系统内核:内核拥有最高权限,可直接访问物理地址
四:指针变量和地址
1.取地址操作符(&)
&:取出内存中数据(变量、数组等)的地址。
& 取出的地址是该数据中第一个字节(地址较小的字节)的地址。
2.指针变量
指针变量是用来专门存放地址(指针)的一个变量
通过&拿到的地址是一个数值,这个数值有时也需要存起来,方便后续使用,那么这个地址就要存放在指针变量中
指针变量是个单独的变量类型,具体写法就是在基本变量类型后加——*
I.指针类型的拆解
inta=0;int*pa=&a;//type * pointer_name = &variable_name//指针指向的对象类型 *说明该变量是指针变量 指针名字 要存的地址II.指针变量的创建与初始化
//eg:int*pa=&a;//创建与初始化- 指针变量也是一种变量,这种变量就是专门用来存地址的,存放在指针变量里的值都会被理解成地址
#include<stdio.h>int*p=NULL- 若创建一个指针变量时,不知道给指针赋什么值或暂时不需要指向变量,可用NULL来初始化
- NULL是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错
- 一个指针变量被初始化为NULL,意思是这个指针变量暂时不指向任何有效的内存位置,等后续使用时的时候,可以给赋值新的地址
- NULL的使用要包含头文件<stdio.h>
- NULL —— 0
- \0 —— 0
- 0 —— 0
- ‘0’ —— 48
- 若指针变量未初始化
- 1.全局:默认会初始化为0(NULL指针)
- 2.局部:野指针(值是未知的),强制使用时,野指针会导致非法访问内存,程序崩溃
III.解引用操作符(*[单目])
只要拿到了地址(指针),就可以通过地址(指针)找到它指向的对象。要完成这类操作,就必须学习一个操作符——解引用操作符(间接访问操作符)*。
inta=;int*pa=&a;printf("%d\n",a);//10——aprintf("%d\n",*pa);//10——*pa——a//解应用,*pa相当于a*pa=100;//a = 100printf("%d\n",a);//100——aprintf("%d\n",*pa);//100——*pa——aIV.指针变量大小
- C语言标准并未规定指针变量的大小(长度)
- x64下:都是8字节
- x86下:都是4字节
变量长度与类型无关
- 指针变量的大小(指针类型的长度)是取决于数据类型的
- 常见数据类型
| 模型\类型 | int | long | long long | pointer(指针) | 操作系统 |
|---|---|---|---|---|---|
| LP33 | 16位 | 32位 | 64位 | 32位 | Win16 API |
| ILP32 | 32位 | 32位 | 64位 | 32位 | Win32 API,Unix及Unix系统 |
| LLP64 | 32位 | 32位 | 64位 | 64位 | Win64 API |
| LP64 | 32位 | 64位 | 64位 | 64位 | Unix及Unix系统 |
32位平台下地址是32个bit位,指针变量大小是4个字节 64位平台下地址是64个bit位,指针变量大小是8个字节- 注:指针变量的大小与类型无关,只要是指针类型的变量,在相同平台下,大小都是相同的
- 指针变量是用来存放地址的
- 地址的存放需多大空间,指针变量的长度就是多大
V.指针类型的意义
【1】指针解引用
指针类型决定了对指针解引用的时候有多大的权限(一次操作几个字节)
| char* | 操作1个字节 |
|---|---|
| short* | 操作2个字节 |
| int* | 操作4个字节 |
转换不同类型的方法
inta=1;char*pa=(char*)&a;【2】指针+,-整数
- 指针+1,其实就是跳过1个指针指向的元素,所以指针+n,就是跳过n个指针指向的元素
- 指针可以+n,也可以-n
intn=10;char*pc=(char*)&n;int*pi=&n;//&n = pc = pi//n = *pc = *pipc+1//= &n + 1pi+1//= &i + 4- 指针的类型决定了指针向前或向后走一步移动多大(距离),指针+,-整数,让指针可以在内存中向前或向后移动,为使用指针访问内存打好了基础
【3】void* 指针
- void* 可理解为无具体类型的指针(泛型指针),这种类型的指针可以用来接收任意类型的地址(不会弹警告)
- 局限性:void*类型的指针不能进行解引用和指针的+,-整数运算
- 后续应用:
- 一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据
五:指针运算
指针+/-整数,指针-指针,指针关系运算
学习指针—>用指针管理内存(读内存中数据;把数据写进内存)
1.指针+/-整数
I.指针访问一维数组
因为数组在内存中是连续存放的,故只需要知道第一个元素的地址,就能顺藤摸瓜找到后面的所有元素
- 以int数组为例
| 数组 | 下标 | 指针 |
|---|---|---|
| 1 | 0 | p |
| 2 | 1 | p+1 |
| 3 | 2 | p+2 |
| 4 | 3 | p+3 |
| 5 | 4 | p+4 |
补充:指针与数组名的区别
初学时很容易把数组名当成指针,二者在「访问元素」上确实很像,但在类型和语义上并不等价。下表从五个维度做个对比(以 int arr[5]; int* p = arr; 为例):
| 对比维度 | 指针 p | 数组名 arr |
|---|---|---|
| 本质 | 一个独立的变量,自身占内存,里面存放的是地址 | 数组首元素的地址,是数组这种类型的「名字」,不是独立变量 |
| 是否可赋值 | 可以,p = p + 1;、p = &arr[2]; 都合法 | 不可以,arr = …会编译报错,数组名是常量地址 |
| sizeof 结果 | 与平台有关:x86 为 4,x64 为 8 | 整个数组的字节数:sizeof(arr) == 5 * sizeof(int) |
| +1 的含义 | 指针向后移动一个元素(int* 即 4 字节) | 同样跳过 4 字节,但 &arr + 1 跳过的是整个数组 |
| 能否取地址 | 可以,&p 得到指针变量自身的地址(二级指针) | 可以,&arr 的类型是 int(*)[5],指向整个数组 |
一句话概括:数组名在大多数表达式中会「退化」成指向首元素的指针,但它本身不是指针变量——它不能被赋值,sizeof 得到的是整个数组的大小。也正因为如此,函数形参写成 int arr[] 时,实际接收到的仍是一个指针,数组长度信息在传参时就丢失了。
II.模拟实现strlen函数
size_tstrlen1(char*s){intcount=0;while(*s){//原因:'\0'ASCII码值为0count++;s++;}returncount;}2.指针 - 指针
指针1 + 整数n = 指针2(向后跳n个元素后的地址)
指针2 - 整数n = 指针1(向前跳n个元素后的地址)
指针2 - 指针1 = 整数n(指针1 - 指针2 = -n)
- 指针 - 指针结果的绝对值是两个 指针间原数的个数
- 两个指针相减运算的前提—>两个指针指向同一块内存,否则无意义(eg:arr)
- 注:指针相减会自动除以sizeof(type)
练习:模拟strlen
size_tstrlen2(char*s){char*str=s;//起始位置char*end=s;//寻\0while(*end){end++;}returnend-str;}3.指针的相关运算
地址的本质是数值,数值可以比较大小,所以也就满足关系运算
现在以数组为例(连续地址【arr】由低到高运算)
#include<stdio.h>intmain(){intarr[]={1,2,3,4,5,6,7,8,9,10};int*p=&arr[0];intsize=sizeof(arr)/sizeof(arr[0]);while(p<&arr[size]){//&arr[0~9] < &arr[10]printf("%d",*p);//虽然arr[10]越界了,但仍能指向(越界危险的原因)p++;//指向的是arr[9]的下一个int大小内存的地址}return0;}六:传值调用与传址调用
一类必须要用到指针的就是传址调用——eg:用函数交换两个数的值
- 传值调用:
- 调用函数时,把实参的值复制给形参
- 形参和实参是两块独立内存
- 函数内修改形参,不影响外部实参
//正常写intmain(){inta=10;intb=20;intz=0;//赋值时会把变量覆盖,因此要引入临时变量z=a;a=b;b=z;return0;}//若想用函数voidswap(inta,intb){intz=0;z=a;//逻辑无问题,但对形参的修改不影响实参a=b;//注:形参与实参的地址也是不同的,形参有单独的地址b=z;}- 传址调用
- 把实参的地址传给形参,形参通常是指针
- 函数通过指针解引用 *p 可以修改外部实参
- 严格来说,C 语言传地址的本质仍然是传值——传的是地址这个值
voidswap2(int*p1,int*p2){intz=0;z=*p1;*p1=*p2;*p2=z;}intmain(){inta=10;intb=20;swap2(&a,&b);return0;}- 结论:
- 传值调用时,实参传递给形参时,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参
- 传址调用时可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量
- 若是函数中只是需要主调函数中的变量值来完成计算,就采用——传值调用
- 若是函数内部要修改主调函数中的变量的值,就采用——传址调用
- 数组传值的本质——传址调用
七:二级指针
二级指针——存指针变量的地址(变量的地址的地址)
inta=10;//普通变量int*pa=&a;//一级指针int**ppa=&pa;//二级指针int***pppa=&ppa//三级指针//以此类推I.分析
inta=10;int*pa=&a;//指针指向的类型——int 表明是指针变量int**ppa=&pa;//指针指向的类型——int* 表明是指针变量II.解引用应用
- 指针相当于加密锁
- 多级指针相当于多级加密锁
#include<stdio.h>intmain(){inta=10;int*pa=&a;int**ppa=&pa;int***pppa=&ppa;printf("%d\n",a);//打印10printf("%d\n",*pa);//打印10printf("%d\n",**ppa);//打印10printf("%d\n",***pppa);//打印10printf("%d\n",****ppppa);//打印10}八:总结
- 内存被划分为以字节为单位的单元,每个单元有唯一编号,这个编号就是地址,也就是指针;内存单元的编号 = 地址 = 指针。
- & 取出的是变量所占内存中第一个字节(地址较小字节)的地址,把它存进变量,就得到了指针变量。
- 指针类型决定两件事:解引用时一次能操作几个字节,以及 +/- 整数时一步跨过多少字节。
- 指针变量的大小与所指类型无关,只与平台有关:x86 为 4 字节,x64 为 8 字节。
- 指针运算建立在「数组在内存中连续存放」之上:p + n 跳过 n 个元素,同一块内存中的两个指针相减得到元素个数。
- 传值调用只复制实参的值,函数内改形参不影响外部;想在函数内部修改外部变量,必须传址调用(传地址)。
- 二级指针存放的是一级指针变量的地址,int** ppa = &pa; 中 * 的层数对应解引用的层数,多一级指针就多一次解引用。
- 学习路径建议:先吃透取地址 & 与解引用 * 这对互逆操作,再动手练指针 +/- 整数与指针相减,最后理解传址调用与二级指针,把「用指针管理内存」这件事真正跑通。