C语言指针全面解析:从内存地址到二级指针
1. 内存和地址
摘要:本文系统讲解了C语言中指针的核心概念与应用。首先从计算机内存管理基础入手,解释了内存单元、地址与虚拟地址空间的关系。接着详细介绍了指针变量的创建、初始化、解引用操作以及指针类型对内存访问的影响。文章深入探讨了指针运算的三种形式(指针±整数、指针-指针、关系运算),并通过数组访问实例展示指针的实际应用。最后分析了传值调用与传址调用的本质区别,以及二级指针的概念与使用场景。全文结合丰富的图示和代码示例,帮助读者建立完整的指针知识体系。
我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。
常见的⼀台电脑上内存是 8GB/16GB/32GB 等,那这些内存空间如何⾼效的管理呢?
我们把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节,然后给每⼀个内存单元⼀个编号,就类似于房间编号。
注意上⾯我们是为了⽅便讲解,对真实的内存访问进⾏了简化的,实际上我们编写的运⾏在X86架构的计算机上的C语⾔程序,是不会直接访问物理内存的。
程序运⾏时为变量分配的地址,在程序调试过程中,我们看到的地址都是虚拟地址;虚拟地址不是物理地址。虚拟地址是虚拟地址空间中的地址。
虚拟地址空间可以看作操作系统为每个程序提供的⼀个“假想的、独占的、连续的内存空间”。
程序中的虚拟地址,会由CPU的内存管理单元(MMU)映射到物理内存中,其实程序最终还是会访问物理内存的。这个由虚拟地址换成物理地址的过程,暂时我们不需要关⼼,学习到操作系统的知识,慢慢再理解就可以了。
2. 指针变量和地址
2.1 取地址操作符(&)
理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间,⽐如:
2.2 指针变量
2.2.1 指针变量和初始化
那我们通过取地址操作符(&)拿到的地址是⼀个数值,⽐如: 0x006FFD70 ,这个数值有时候也是需要存储起来,⽅便后期再使⽤的。
这里我们创建一个变量pa,存放a的地址。pa是用来存放地址的变量,而地址又叫指针,所以该变量又叫指针变量。
a的类型是int ,而pa的类型是int * pa。
有时候我们创建了⼀个指针变量,但是暂时不需要指向变量,可以使⽤ NULL 来初始化, NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。⼀个指针变量被初始化为 NULL ,意思就是这个指针变量暂时不指向任何有效的内存位置,等后续使
⽤的时候,可以给赋值新的地址。
2.2.3 解引⽤操作符
在现实⽣活中,我们使⽤地址要找到⼀个房间,在房间⾥可以拿去或者存放物品。
程序中我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?
C语⾔中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符( * )。
2.3 指针变量的⼤⼩
C语⾔标准并未规定指针变量的⻓度,我们现在分别在VS2026的,X86和X64环境下测试:
其实指针变量的⼤⼩是取决于数据模型的,下⾯列出了常⻅的4个数据模型:
数据模型指的是在特定操作系统、CPU 架构和 ABI(应⽤程序⼆进制接⼝)下,C/C++ 等语⾔中基本
整数类型( short 、 int 、 long 、 long long )以及指针类型的具体位宽(字节数) 的约定。
指针变量是用来存放地址的,地址的存放需要多大空间,指针变量长度就多大
3. 指针类型的意义
指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的,为什么还要有各
种各样的指针类型呢?
我们可以看出, char* 类型的指针变量 +1 跳过 1 个字节, int* 类型的指针变量 +1 跳过了 4
个字节。这就是指针变量的类型差异带来的变化。
指针 +1 ,其实跳过 1 个指针指向的元素,所以指针 +n ,就是跳过 n 个指针指向的元素。
指针可以 +n ,那也可以 -n 。
结论:指针的类型决定了指针向前或者向后⾛⼀步⾛多⼤(距离)。
指针±整数,让指针可以在内存中向前或者向后移动,这就为使⽤指针访问内存打好了基础。
3.3 void* 指针
在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为⽆具体类型的指针(或者叫泛型指
针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性,void类型的指针不能直接进⾏指针的±整数和解引⽤的运算。*
void* pa 无具体类型,所以解引用不知道访问几个字节。
真的要使用可以这样:
((int)pa) = 100;
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以
实现泛型编程的效果。使得⼀个函数来处理多种类型的数据,在后期的课程中我们会再次讲解。
这里我们重申指针学习的目的:用指针来管理内容
1 读取内存数据
2 数据写入内存
- 指针运算
指针的基本运算有三种,分别是:
指针± 整数
指针-指针
指针的关系运算
4.1 指针± 整数
4.1.1 指针访问⼀维数组
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。
4.2 指针 - 指针
4.2.1 为什么会有指针-指针
⽇期1 + 天数 = ⽇期2
⽇期2 + 天数 = ⽇期1
⽇期2 - ⽇期1 = 天数
和⽇期运算的道理⼀样,指针也可以进⾏类似的运算
指针1 + 整数n = 指针2 (向后跳过n个元素后的地址)
指针2 - 整数n = 指针1 (向前跳过n个元素后的地址)
指针2 - 指针1 = 整数n (指针1 - 指针2 = -n)
指针 - 指针 结果的绝对值是两个指针之间元素的个数。
两个指针相减运算的前提是两个指针指向同⼀块内存空间,否则计算没有意义。
4.3 指针的关系运算
地址的本质还是数值,数值是可以⽐较⼤⼩的,所以也就满⾜关系运算。
这次我们重新改造⼀下前⾯写过的指针访问⼀维数组的代码,因为数组的元素的地址随着下标的增⻓在不断地变⼤的,所以 &arr[0] < &arr[sz] 的,所以我们就可以使⽤指针的关系运算来换⼀种⽅式来实现。
5. 传值调⽤和传址调用
学习指针的⽬的是使⽤指针解决问题,那什么问题,⾮指针不可呢?
我们发现在main函数内部,创建了a和b,a的地址是0x00cffdd0,b的地址是0x00cffdc4,在调⽤Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是x的地址是0x00cffcec,y的地址是0x00cffcf0,x和y确实接收到了a和b的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值,⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。
Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式叫传值调⽤。我们在学习函数时编写的函数基本都是传值调⽤。
那怎么办呢?
我们现在要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接
将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。
6. ⼆级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪⾥?
这就是 ⼆级指针 。