深入理解指针(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*类型的指针是使用在函数参数的部分,用来接收不用类型数据的地址。这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据。

相关推荐
MQLYES28 分钟前
03-BTC-数据结构
数据结构·算法·哈希算法
无限进步_38 分钟前
【数据结构&C语言】对称二叉树的递归之美:镜像世界的探索
c语言·开发语言·数据结构·c++·算法·github·visual studio
im_AMBER1 小时前
Leetcode 98 从链表中移除在数组中存在的节点
c++·笔记·学习·算法·leetcode·链表
高山上有一只小老虎1 小时前
灵异背包?
java·算法
s09071362 小时前
【综述】前视二维多波束成像声呐(FLS)图像处理算法全解析:从成像到深度学习
图像处理·人工智能·算法·声呐·前视多波束
Eternity∞2 小时前
基于Linux系统vim编译器情况下的C语言学习
linux·c语言·开发语言·学习·vim
星河耀银海2 小时前
人工智能从入门到精通:机器学习基础算法实战与应用
人工智能·算法·机器学习
nice_lcj5202 小时前
数据结构之堆:从概念到应用全解析(附TOP-K经典问题)
java·数据结构·算法
无言(* ̄(エ) ̄)2 小时前
进程---Linux/C语言
java·开发语言·算法
漫随流水2 小时前
leetcode算法(429.N叉树的层序遍历)
数据结构·算法·leetcode·二叉树