深入理解指针(1)

一、内存和地址

(一)内存

首先,我们先举一个生活中的案例:

假设有一栋宿舍楼,把你放在楼里,楼上有100个房间,但是房间没有编号,如果一个朋友来找你玩,就得按个房间去找,这样的效率是非常低下的。但是我们如果根据楼层和楼层的房间情况,给每个房间编号,如:A101。

有了房间号,我们就可以迅速地找到每一个房间。

如果把上述的例子对照到计算机中又会怎么样呢?

计算机上的CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放在内存中,所以我们买电脑的时候,会关注电脑上的内存大小,如:8GB、16GB、32GB等等。那么,内存空间如何高效管理呢?

我们可以吧内存划分为一个个内存单元,每个内存单元的大小取1个字节

我们知道,一个比特位可以存储一个2进制数字,即0或1。

计算机中常见的单位:

其中,每个内存单元相当于一个学生宿舍,一个字节空间里面能放8个比特位,就好比同学们住的八人间,每个人就是一个比特位。

每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到一个内存空间。

生活中,我们把门牌号也叫做地址,在计算机中我们把内存单元的编号也称为地址。

C语言中,我们给地址起的新名字叫作:指针。

所以我们可以这么理解:

内存单元的编号==地址==指针

(二)究竟该如何理解编址

首先,我们必须理解,计算机内是有很多硬件单元,而硬件单元是要互相协同工作的所谓协同,至少相互之间要能够进行数据传递。

但是硬件与硬件之间是相互独立的,那么如何通信呢?

答案就是,用"线"连起来。

CPU和内存之间也是有大量数据交互的,所以二者也必须用线连起来。

不过,我们今天关心一组线,叫作地址总线。

CPU访问内存中某个字节的空间,就必须知道这个字节空间在内存的什么位置,而因为内存中有很多字节,所以需要给内存编址(就如同宿舍很多,需要给宿舍编号)。

计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。

就比如,钢琴,古筝等乐器上面并没有写音素的位具体发音位置,但是演奏者照样能准确的找到每一个位置,这是为何?因为制造商在"硬件设计"时,就已经设计好了,并且所有演奏者都知道。

本质就是一种约定出来的共识。

硬件编址也是如此。

我们可以简单理解,32为机器有32根地址总线,没跟先只有两态,表示0,1【电脉冲的有无】,那么一根线就能表示2种含义,2根线能表示4种含义,32根线就能表示2^32中含义,每一种含义都代表一个地址。

地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据通过数据总线传入CPU内寄存器。

二、指针变量和地址

(一)取地址操作符(&)

理解了内存与地址的关系,我们再回到C语言,在C语言中创建变量其实就是像内存申请空间,比如:

cpp 复制代码
#include <stdio.h>
int main()
{
	int a = 520;
	return 0;
}

比如,上述代码就是创建了整型变量a,内存中申请了4个字节,用于存放整数520,其中每个字节都有地址。

那我们如何能得到a的地址呢?

这里就需要用到取地址操作符(&):

cpp 复制代码
int main()
{
	int a = 520;
	printf("%p\n", &a);
	return 0;
}

我们可以发现,虽然整型变量占用4个字节,但是打印出来的地址却只有第一个。

这是因为&a取出的是a所占4个字节中地址较小的字节的地址。

虽然只取出一个地址,但是我们只要知道了第一个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。

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

1.指针变量

我们通过取地址操作符(&)拿到的地址是一个数值,比如0xF006FFD70,这个数值有时候也是需要存储起来,方便后期使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量中。

比如:

cpp 复制代码
int main()
{
	int a = 520;
	int* pa = &a;
	return 0;
}

指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址

2.如何拆解指针类型

cpp 复制代码
int a = 520;
int* pa = &a;

pa左边写的是int*,我们需要分开来看:

*是在说明pa是一个指针变量;

而int是在说明pa指向的是整形(int)类型的对象。

由此类推,我们可以得到char类型的指针变量:char*。

3.解引用操作符(*)

我们将地址保存起来,未来是要使用的,那怎么使用呢?

在C语言中,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习一个操作符叫解引用操作符(*)。

cpp 复制代码
int main()
{
	int a = 520;
	int* pa = &a;
	*pa = 1314;
	printf("%d", a);
	return 0;
}

上述代码中,*pa的意思就是通过pa存放的地址,找到指向的空间,*pa其实就是a变量了;所以*pa=0,这个操作符就把a改成了0。

这里,我们不禁回想,那我直接a=0岂不是更简单?为什么非得用指针呢?

指针为我们提供了另外一种修改a的途径,写代码就会更加灵活,慢慢地就能够意识并理解了。

3.指针变量的大小

根据前面内容,我们可以了解到32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后就是1或者0,那么我们把32根地址线产生的二进制序列当做一个地址,那么地址就是32个比特位,需要4个字节才能存储。

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

同理,如果是64位机器,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS 1
int main()
{
	printf("%zd\n", sizeof(char*));
	printf("%zd\n", sizeof(int*));
	printf("%zd\n", sizeof(short*));
	printf("%zd\n", sizeof(double*));
	return 0;
}

指针变量的大小和类型是无关的,只要是指针类型的变量,在相同的平台下,大小是相同的。

三、指针变量的类型和意义

(一)指针的解引用

对比代码一与代码二,观察调试时二者内存的变化:

cpp 复制代码
//代码一
int main()
{
	int n = 0x11223344;
	int* pn = &n;
	*pn = 0;
	return 0;
}
cpp 复制代码
//代码二
int main()
{
	int n = 0x11223344;
	char* pn = &n;
	*pn = 0;
	return 0;
}

通过调试我们可以看到,代码1会将整型n的4个字节全部改为0,但是代码2只是将字符型n的第一个字节改为0。

结论:指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)。

比如:char*的指针解引用只能访问一个字节,而int*的指针解引用就能访问四个字节。

(二)指针+-整数

cpp 复制代码
int main()
{
	int n = 10;
	char* pc = (char*)& n;
	int* pi = &n;
	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc+1);
	printf("%p\n", pi);
	printf("%p\n", pi+1);
	return 0;
}

我们可以看出,char*类型的指针变量+1跳过1个字节,int*类型的指针变量+1跳过了四个字节。

这就是指针变量差异带来的变化。指针+1,其实是跳过1个指针指向的元素。指针可以+1,那也可以-1。

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

(三)void*指针

在指针变量中有一种特殊的类型是void*类型的,可以理解为无具体类型的指针(或者叫作泛型指针),这种类型的指针可以接收任何类型的地址。但是也有局限性,void*类型的指针不能进行指针的+-整数和解引用运算。

cpp 复制代码
int main()
{
	int a = 10;
	int* pa = &a;
	char* pc = &a;
	return 0;
}

上述代码中,将一个int*类型的变量赋值给一个char*类型的指针变量,编译器会报警告,这是因为类型不兼容。而用void*类型就不会有这样的问题。

cpp 复制代码
int main()
{
	int a = 10;
	int* pa = &a;
	void* pc = &a;
	*pa = 10;
	*pc = 0;
	return 0;
}

这里我们可以看到,void*类型的指针可以接受不同类型的地址,但是无法直接进行指针运算。

那么void*类型的指针到底有什么用呢?

一般void*类型的指针是使用在函数参数的部分,用来接收不用类型数据的地址。这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据。

相关推荐
WWZZ20251 小时前
快速上手大模型:深度学习13(文本预处理、语言模型、RNN、GRU、LSTM、seq2seq)
人工智能·深度学习·算法·语言模型·自然语言处理·大模型·具身智能
Christo32 小时前
AAAI-2024《Multi-Class Support Vector Machine with Maximizing Minimum Margin》
人工智能·算法·机器学习·支持向量机·数据挖掘
元亓亓亓3 小时前
LeetCode热题100--79. 单词搜索
算法·leetcode·职场和发展
hazy1k3 小时前
ESP32基础-Socket通信 (TCP/UDP)
c语言·单片机·嵌入式硬件·网络协议·tcp/ip·udp·esp32
dvvvvvw3 小时前
x的y次幂的递归函数.c
c语言
司铭鸿3 小时前
化学式解析的算法之美:从原子计数到栈的巧妙运用
linux·运维·服务器·算法·动态规划·代理模式·哈希算法
2501_941148614 小时前
人工智能赋能智慧物流互联网应用:智能仓储、配送优化与供应链管理实践探索》
visual studio
ekprada4 小时前
DAY 18 推断聚类后簇的类型
算法·机器学习·支持向量机
生信大表哥4 小时前
Python单细胞分析-基于leiden算法的降维聚类
linux·python·算法·生信·数信院生信服务器·生信云服务器