指针(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.

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

相关推荐
AskHarries2 小时前
把一个外部系统接成 MCP 工具
后端·程序员
threerocks3 小时前
AI编程的商业模式已经在互联网大厂跑通了
程序员·aigc·ai编程
用户526835677903 小时前
云原生落地:如何配置 Alertmanager 插件,将 Prometheus 告警直接打通至硬件声光语音终端?
程序员
用户852495071843 小时前
我跟 AI 说了名字它转头就忘,后来我手动给它加了个"记忆"
程序员
zzzzzz3103 小时前
当甲方说'logo放大的同时再缩小一点'时,我用 AI 把这个需求做出来了
javascript·css·程序员
Hilaku3 小时前
Node.js 还能再战十年?给你一个不换引擎的理由
前端·javascript·程序员
Hyyy16 小时前
token是什么?为什么大模型会有上下文长度的限制
程序员·llm·ai编程
程序员cxuan20 小时前
幽默,一个 Github 名字叫“马尾辫”,但是他给你省了 80% 的 token
人工智能·后端·程序员
kartjim1 天前
我用 AI 一小时写了一个世界杯数据可视化平台|前端 VibeCoding 初体验
前端·程序员·ai编程
SimonKing1 天前
艹,维护AI写的代码,我心态崩了......
java·后端·程序员