指针(1)从 0 到 1:C 语言指针入门全解析 —— 内存、地址与指针操作详解

前言

相信每一个C语言的新手,都曾在指针面前感到困惑:

1.地址到底是什么?它和指针有什么区别? 2. * 和 & 这两个符号到底在干嘛?

这些问题的根源,在于我们还没有建立起对内存和地址的直观认知。指针,正是 C 语言连接底层硬件和上层逻辑的桥梁。掌握了指针,你才能真正理解 C 语言的 "自由" 与 "强大",写出高效、灵活的代码,而不是被各种语法规则束缚。

今天,我们就从最底层的内存和地址讲起,带你一步步揭开指针的神秘面纱,从 0 到 1 打牢指针基础。

1.内存和地址

在讲内存和地址前,我们先想一个生活中的案例:

在一栋楼里面,有若干个小房间。假如说这些房间没有编号,那么我们想要寻找一个特定的房间需要挨个楼层,逐个房间的寻找,效率非常低,而如果将这些房间都打上编号,那么根据房间的编号,我们想要寻找一个房间将会变得轻而易举。

在生活中,每个房间有了编号,我们找房间的效率会得到大大的提升。

那么把上面的例子对应到计算机中,又是什么怎么样呢?

在cpu处理数据时,需要的数据是从内从中读取的,处理后的数据也会重新放回内存中,那么内存空间该怎么进行高效的管理呢?

内存被划分为一个个内存空间,每个内存空间都有其相应的编号(也就是地址),在存取数据时,只需要在内存中找到相应内存空间的编号,然后就可以以次存取数据。

其实内存空间的管理与上述例子十分相似,我们可以把内存看成一栋楼,而在楼里面划分成的一个个小房间就是一个个内存单元,每一个小房间都有其相应的编号,而编号就是这个内存空间的地址,我们可以通过这个地址直接找到所对应的内存空间。

在c语言中内存空间的地址是用指针来存储的,指针里面存储的是地址,也就是可以这么理解,指针相当于一张纸,这张纸上面所写的内容是地址,我们可以通过这张纸来寻找所对应的内存空间。

我们常说的"一个内存单元",它的大小是1个字节(Byte),这是内存中可寻址的最小单位。

一个int类型的变量,它会占用四个连续的内存单元,而它的地址是第一个内存单元所对应的地址。

2.指针变量和地址

2.1取地址操作符(&)

理解了地址和内存的关系,接下来我们就回到C语言中,在C语言中创建一个变量就是在内存中申请开辟一块空间。接下来让我们用一个例子来理解一下:

c 复制代码
#include<stdio.h>

int main()
{
	int a = 10;
	return 0;
}

上述代码,int a = 10,创建了一个整形变量a,当这个变量被创建时,会向内存中申请开辟一块大小位4个字节的内存空间,这四个字节被用来存储整数10,这四个字节的每个字节都有其对应的地址,这时我们可以通过调试来直观的观察一下:

上述代码中,整型变量 a 的起始地址为 0x000000AAC4CFFCA4,在内存窗口中可以看到从该地址开始的 4 个字节依次为:

在讲解之前,我们需要来补充一个知识点:在计算机中,数据始终都是以二进制的形式存储在内存中的,但二进制数往往非常冗长,不便于我们观察,因此我们通常会用十六进制数来代替二进制数,以此来简化表示。

这背后的换算关系是:

  1. 1 字节(Byte)= 8比特(bit),而1比特就是一个二进制位。
  2. 1 个十六进制位可以表示4个二进制位。
  3. 因此 个字节可以用个十六进制位来表示。

现在我们回到代码int a = 10,

  1. 十进制的10用十六进制来表示位a

  2. 整形变量a向内存中申请开辟了四个字节的大小,一个字节用两个十六进制位来表示,因此需要8个十六进制位来表示,在不影响数据大小的前提下,只能在a前面补上7个0,因此10在内存中应该表示位,0x00 00 00 0a,

  3. 又因为在vs的编译环境下,数据在内存中是以小端模式进行存储的,即低字节的数据存储在低字节的地址里,高字节的数据存储在高字节的地址里

  4. 因此,0x0000000a 这个 4 字节数据,在内存中的存储顺序就变成了 0a 00 00 00,这与我们在调试窗口中看到的结果完全一致。

那么如何得到整型变量a的地址呢?这就需要一个操作符来帮忙------&(取地址操作符)。

c 复制代码
#include<stdio.h>

int main()
{
	int a = 10;
	printf("%p", &a);
	return 0;
}

&a取出的是四个字节中地址较小的那个字节的地址,对于地址的打印需要使用%p 根据上图我们就可以印证上面所说的结论。

虽然说整型变量a占用了4个字节,但是我们只需要知道了第一个字节的地址,我们就可以顺藤摸瓜的找到剩下三个字节,从而读取或者修改这个变量的值。

2.2指针变量和(*)解引用操作符

2.2.1指针变量

现在我们已经知道了,通过&操作符,我们可以得到一个变量的地址,一个变量的地址是一个数值,有的时候我们也需要把这个数值存储起来,方便后续的使用,那么这样的地址值应该存放在哪里呢?答案是:指针变量中。 如下:

c 复制代码
#include<stdio.h>

int main()
{
	int a = 0;
	int* p = &a;
	printf("%p\n", &a);
	printf("%p\n", p);
	return 0;
}

通过上述代码和运行结果我们可以清楚的观察到:

  1. printf("%p\n",&a),打印的是a的地址。
  2. printf("%p\n",p),打印的是指针变量p的值。
  3. 通过代码的运行,我们可以观察到两者的值是相等的,由此也就印证了p中存放的确实是a的地址。

指针变量其实也是一种变量,它是专门用来存储地址的变量,存放在指针变量中的值都会被理解为地址。这就是指针变量的强大之处,我们可以通过指针变量来灵活的操作和传递内存地址。

2.2.2指针变量的类型

通过上述代码我们会发现 p 的前面有一个int*,这又是什么呢?

为了弄清这个问题,我们可以类比上面的 int a 。a是一个变量,把 a 拿走后,剩下一个 int,而这个int 就是 a 的类型(即整型)。那么 int* p 也是如此,p是一个变量(指针变量),把 p 拿走后剩下一个int*,而这(int*)就是 p 的类型。

弄清了 p 的类型后,我们接下来深入理解一下 int* p ,* 是在说明 p 是一个指针变量,int 则是在说明 p 这个指针变量存储的是一个整型的地址。(即p指向的是一个整型类型的对象)

当然,指针变量也和其他变量一样不仅仅只有 int* 这一种类型,它的类型是由它所存储的地址所指向的对象的类型来决定的,例如:

c 复制代码
char ch  = 'a';
char* pc = &ch;//ch是字符类型,存储它的地址就应该使用char*类型的指针。
double d = 3.14;
double* pd = &d;////d是double类型,存储它的地址就应该使用double*类型的指针。

2.2.3解引用操作符

在现实生活中我们通过房间号就可以找到一个房间,然后从房间中拿取或者存储物品。

在c语言中也是一样,指针变量存储着一个数据的地址,我们通过指针变量就可以来读取或者修改它所指向的对象,而实现这一操作的关键就是解引用操作符(*),

c 复制代码
#include<stdio.h>

int main()
{
	int a = 10;
	int* p = &a;
	printf("%d\n", a);
	printf("%d\n", *p);
	*p = 0;
	printf("%d\n", *p);
	printf("%d\n", a);
	return 0;
}

上述代码中*p ,其实就是解引用操作符的应用,p它的意思是通过p中存放的地址,来找到该地址所对应的内存对象(即整型变量a),也可以这么理解,即p就是变量a。

通过观察代码的运行结果,我们发现*p 和 a 的值始终相等,当我们修改 p 的值时,a的值也随之被修改,这也就印证着上面所说的p就是变量a。

当然我们难免会这样想,这⾥如果⽬的就是把a改成0的话,写成 a = 0; 不就完了,为啥⾮要使⽤指针呢?

其实这⾥是把a的修改交给了pa来操作,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活,比如在函数传参时,我们无法直接修改外部变量的名字,但通过传递地址(指针),就能借助解引用操作修改外部变量的值;在数组遍历、动态内存管理等场景中,指针更是不可或缺的工具。这些场景的优势,我们会在后续的学习中逐步体会到。

2.3指针变量的大小

对于32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。

如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。

同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的⼤⼩就是8个字节。

接下来我们可以通过代码来验证一下。

c 复制代码
#include<stdio.h>

int main()
{
	printf("%d\n", sizeof(int*));
	printf("%d\n", sizeof(char*));
	printf("%d\n", sizeof(double*));
	printf("%d\n", sizeof(float*));

	return 0;
}

x86环境输出结果(32位机器) x64环境输出结果(64位机器)

通过对代码及运行结果的观察,我们不难发现在相同的环境(也可以说平台)下,不同的指针类型的大小是相同的。

由此我们可以得出结论:

  1. 32位平台下,地址是32个bit,所对应指针变量大小为4;
  2. 64位平台下,地址是64个bit,所对应指针变量大小为8;
  3. 指针变量的大小和类型时无关的,在相同的平台下,指针类型的大小时相等的。

3.指针变量类型的意义

根据上面所学,我们知道指针变量的大小和类型无关,只要是指针变量,在同一平台下,大小都是相等的。那为什么还要有各种各样的指针类型呢?

其实指针的类型是有其特殊意义的,接下来就让我们继续学习。

3.1指针的解引用

让我们先来观察下面的一段代码:

c 复制代码
#include<stdio.h>

int main()
{
	int a = 0x11223344;
	int* pa = &a;
	char* pc = (char*) & a;
	printf("%x\n", *pa);
	printf("%x\n", *pc);
	return 0;
}

注意:0x表明这是一个十六进制数。%x是将数据以十六进制的形式打印。

我们先来分析一下代码,根据前面我们所学的知识,对指针解引用,其实就是通过指针来访问它所指向的内存数据。

那么我们不难想到,对于 *pa 而言它的值与 a 相等是11223344,将代码运行起来后,我们发现结果确实如此。

按理来说,*pc应该与 *pa一样,因为他们所指向的都是变量a ,*pc的值也应该是11223344,但是当代码运行起来后我们发现,事实并非如此,*pc的值为44,那这是为什么呢?

究其根本其实是指针变量的类型在影响结果:

变量 a 的类型是int,int 类型的变量在内存中占用4个字节,存储的十六进制为0x11223344,

对于int*类型的指针变量而言,它在解引用时会从指向的地址开始访问 个字节的内存,因此pa刚好能完整读取完 a 的所有字节,得到完整值 0x11223344 ,

而对于char*类型的指针变量而言,它在解引用时会从指向的地址开始访问 个字节的内存,因此 pc 只能访问到 a 中一部分字节的内容。

又因为在vs的编译环境下,数据是以小端模式(低字节数据存放在低地址处)存储的,0x11223344的四个字节会按 0x44(低地址),0x33, 0x22, 0x11(高地址),的顺序存放;而指针解引用默认从低地址处进行访问,因此 *pc 读取到的就是低地址处的一个字节 0x44.

结论:指针类型决定了对指针解引用时,一次操作指针所能访问的内存字节数。

3.2指针+-整数

为了了解指针+-整数的效果,我们来通过一段代码观察一下。

c 复制代码
#include<stdio.h>

int main()
{
	int a = 10;
	int* pa = &a;
	char* pc = (char*)&a;
	printf("%p\n", &a);
	printf("%p\n", pa);
	printf("%p\n", pa+1);
	printf("%p\n", pc);
	printf("%p\n", pc+1);

	return 0;
}

通过观察上述代码及运行结果,我们不难发现&a ,pa,pc这三个的地址是相同的,pa + 1相较于pa大了四个字节,pc + 1相较于 pc 大了一个字节。

它的本质其实就是char类型的指针变量+1,跳过一个字节,int类型的指针变量+1,跳过四个字节。这就是指针变量类型差异所带来的变化,指针+1,其实就是跳过一个指针所指向的元素类型的大小。指针可以+1,也可以-1.

结论:指针的类型决定了指针向前或向后走一步有多大(距离)。

相关推荐
陈随易2 小时前
CDN的妙用,隐藏接口IP,防DDOS攻击
前端·后端·程序员
修己xj13 小时前
从少年到父亲:我在异乡的第一个年
程序员
阿里嘎多学长17 小时前
2026-02-07 GitHub 热点项目精选
开发语言·程序员·github·代码托管
阿里嘎多学长1 天前
2026-02-12 GitHub 热点项目精选
开发语言·程序员·github·代码托管
mCell1 天前
如何零成本搭建个人站点
前端·程序员·github
程序员鱼皮2 天前
我用 GLM-5 做了个 AI 女友,能发自拍、发语音、还能帮我干活!
程序员·aigc·ai编程
程序员鱼皮2 天前
40 个 Agent Skills 精选资源:入门教程 + 实用工具 + 必装推荐
前端·后端·计算机·ai·程序员·互联网·编程
程序员洪志道2 天前
封装复杂性:一个反复生效的架构手法
nginx·程序员