[C/C++] 指针的原理和对指针的运用及理解(包括函数指针和多级指针)


C/C++指针存在的必要性

指针的重要性,是毋庸置疑的.
有很多时候你都需要对内存进行管理,没有指针,就有受苦的时候了.
没有指针,依托节点数据结构铸造出来的数据类型,就是无稽之谈.
总不可能为了实现这些非线性数据结构,去写内联汇编吧.

C/C++和很多语言最大的区别,就是对于指针的运用.

C/C++不像其他语言哪样,对程序员保护的像个小宝宝一样,生怕你让自己的程序变成内存毁灭者.
它们给与程序员极高的自由,只要遵守语法规则,即便是让内存乱成一锅粥,也无所谓.
高自由度,也带来了高混乱,如果你没有搞懂数据类型间的权限关系,级别,就会发现,自己只是在写可以编译的bug.


内存和指针原理的简易认知

指针的原理直指内存,所以需要先对内存有一个概念.

计算机之所以能进行工作,是因为它拥有存储空间,以来进行对其上存储的数据进行读写操作.
如果计算机没有任何意义上的存储单位,那么它就是个砖头,毕竟做个1+1,还得让它能有地方放数字呢.

计算机的底层语言是机器语言,这是计算机唯一真正能看懂的东西.
也就是二进制.
之所以计算机采用二进制是因为这和它的存储方式有关系.
最小的存储单位是 位元(英文:bit 简称:位)
最小的使用单位是 字节(英文:byte)

存储单位(位)对应关系位数量(bit)
位(bit)1 bit1
字节(byte)8 bit8
千字节(KB)1024 byte8192
兆字节(MB)1024 KB8,388,608
千兆节(GB)1024 MB8,589,934,592
太兆节(TB)1024 GB8,796,093,022,208
拍字节(PB)1024 TB9,007,199,254,740,992

程序员最小可操控的存储单位是bit,但一般不会涉及到.
正常的最小操控都是以字节为单位.

2021年,个人电脑,最小都是TB为单位的存储容器吧.
不管它是机械还是固态.
这里面存储的位,都太过于庞当了.
而为了管理这些存储,就有了存储地址.

不过这是外存(断电数据不归零)
程序员一般和内存打交道.(断电数据归零)

但是存储单位是一样的.

你的程序平时存在外存上,而在运行的时候,会被copy到内存中,等待Cpu临幸.

32位程序会分配一个4GB大小的虚拟内存(寻址空间).
0x0000 0000 - 0xFFFF FFFF
64位则是这么大
0x0000 0000 0000 0000 0xFFFF FFFF FFFF FFFF

指针的长度,在32位系统上,是4字节,64位系统则是8字节.
指针作为存储地址的变量,那么首先必须得先可以存这么大的数才可以.
2^32 : 0XFFFF FFFF
2^64 : 0XFFFF FFFF FFFF FFFF

这些虚拟内存,不是全给你用的.有 一部分会被系统拿走.
然后虚拟内存地址会映射到实际的物理存储上.

而内存中,又分为多个区段.
栈区,堆区,数据区,代码区,静态区(static修饰 就是放到这里),字符常量区
你的指针指的就是这些内存地址.

指针,字面的意义上的理解,就是一根针,指向一个东西.
指的就是内存地址.
可以理解为指针类型,就是个变量,只不过这个变量,只允许放地址.
内存地址是一段16进制的正整数,当然,你也可以给它换算成其他进制.
但是16进制作为存储地址,是有它的道理的.
0xFF正好表达1字节最大数.
那么0x00-0xFF就是1字节的范围.

指针的理解

首先我们需要明确的一点是,指针是独立的数据类形.
而我们常说的整数型指针,字符型指针,浮点型指针,这指的是指针读取值地址中的数据方式.
计算机中的数据类型,归根结底只有两种,那就是整数型和浮点型.(指针就是整数类型)
这就涉及到关于计算机中是如何存储数据的知识点了.
不在本章讨论范围内.

不要因为int* short* char*就觉得指针类型,是依托于这些数据类型下的.
指针独立于这些数据类型,不存在任何的从属关系.

最简单的证明,你可以sizeof一下指针,32位是4字节,64字节是8字节.
所有指针,在程序位元相同的情况下,占用的字节,永远是一样的.

作为指针类型的变量,它只接受内存地址.

指针相关运算符

取地址运算符:&[右值] 返回右值自身地址
取值运算符:*[右值] 返回右值指针的值中值给左值

取地址运算符&也是引用运算符.
关于引用的详细信息:引用和指针的关系
取值运算符*也是指针级数运算符.
声明指针的时候 数据类型之后的就是指针级数.
一级指针 *
二级指针 **
以此类推

dataType* varName {address}; // 一级指针
dataType** varName {address}; // 二级指针

除了上面的这些,还有一种就是 p->xxxx
不过这个运算符只有类才会常见.
不在本章讨论范围


数据指针和函数指针的声明方式

数据指针的声明方式
[数据类型] [指针级数] [指针变量名称] = [地址] C Style

int* pointer = 0x66666666;

个人叫法:先知赋值. // 因为你需要未卜先知才行(如果你明确这个地址是有效的,那么,你从事的行业就有点意思了)
正常叫法:常量赋值;
这是一种非常少用的赋值方式,直接用常量来为指针赋值.
一般这种赋值方式常见在获取对方程序数据地址后(XDebug,CheatEngine这种调试工具)
写第三方DLL,通过该指针来对改地址内的数据进行读写操作.
虽然这种赋值方式有很多的不确定性,极有可能引发整个程序的崩溃(内存泄漏,权限冲突)
但这种方式,确实是合法的.
就如同之前说过的一样,指针只接受内存地址,而内存地址,就是整数类型.
而整数常量,就是整数类型,完全有理有据

注意!
这种赋值方式,只能是底层数据类型是整数类型的才可以.
也就是 char,short,int,long 这些.
换言之,void,float,double这些非整数类型,是不可以用这种方式的.

int* pointer = &variable; // 使用取地址运算符 获得变量的地址,然后

正常叫法:变量地址赋值.
这是最常见的赋值方式.
通过取变量地址的方式,来进行赋值.
这种方式的优点就是,精准的获取变量地址.
不管程序启动多少次,这个变量的地址是否发生变动,都可以正常的运行.
但是也会对该变量类型进行检测.(编译器检测)
例子:

[数据类型] [指针级数] [指针变量名称]  = [地址]
  int         *          point       &variable

编译器会去检测variable变量,是否是int类型.
这是为了防止指针不能以正确的方式读取该地址的数据.

void* point = 任何数据类型

个人叫法:万能指针赋值.
将数据类型写成void类型之后,编译器就不会对地址的来源变量,进行数据类型检测.
除非你真的想不出应该用什么数据类型来作为指针的读写方式,不然,绝对不推荐这种声明方式.
很容易导致数据读写冲突.

[数据类型] [指针级数] [指针变量名称] {地址} C++ Style

int* pointer {0x66666666};

这是C++的声明方式,你可以将赋值号去掉,换成花括号.

指针的使用注意!!!
永远不要让指针变野指针.
指向的地址永远要有掌控能力,你必须指到指针会指向什么.

如果你申请了一个指针,不能立马给它赋地址,那你就应该用如下方式来声明.
不然鬼知道这片地址划给你前里面放的是个什么东西.

int* point = 0x0; //C Style    0x0代表空地址,任何读写方式的指针都可以直接指向这个常量
int* point {nullptr}; // C++ Style nullptr是C++才有的关键字,空指针的意思

函数指针的声明方式
typedef [数据返回类型] ([指针级数] [指针变量名称]) ([参数类型]) C Style
using [指针变量名称] = [数据返回类型] ([指针级数] ) ([参数类型]) C++ Style

int Add(const int &a,const int &b)
{
    return a + b;
}

typedef int(*int_point_int_int)(const int&,const int&);		// C Style
using Int_Point_Int_Int = int(*)(const int&, const int&);	// C++ Style

int main(void)
{
	const int_point_int_int ipiiC = Add;	
    const Int_Point_Int_Int ipiiCpp{ Add };
    std::cout << ipii(1, 1) << std::endl;
    return 0;
}

函数赋值给指针的时候,不用写取地址运算符,因为函数名就是个指针.
函数名是个一级指针.
汇编代码就能看到

汇编例子:
xxx xxxx 如果函数是外平栈 这就是平栈汇编
call xxxxxx
xxx xxxx

call的就是函数地址, 而地址,在C/C++中就是个一级指针的级别.
如果你写取地址运算符,也没有关系,编译器理解你想要啥的.

如果你不明白编译器凭啥可以理解的你的意思.
其实也很简单,毕竟你也做不了别的.
也别不信邪,这是在原理上不允许的.

你总不至于想着取函数地址里面放的东西吧.
函数名这个一级指针里面放的东西是函数的代码段首地址
从函数头开始一直到retn返回道
这一整个区段,都可以理解为是函数名这个一级指针的值,编译器还想问,你凭啥觉得你能取这玩意.lol


指针原理

其实这个标题并不准确,因为上面也有很多关于指针的原理.
但是有些东西吧,就是和原理有牵连,所以只好就在上面一块讲了.
指针&内存
一块内存可以被很多指针指向
而指针本身,也有属于自己的内存空间.
它的工作原理就是在计算机申请一块内存空间,然后这个内存空间放的是另一个内存空间的地址.
我们可以通过指针来对其里面的内存空间进行有权限(读写)操作.

内存地址是盒子编号,盒子里面放的东西是值.
这个盒子里面可能又放了一个盒子.这个小盒子,同样叫做值.
然后这个小盒子里的东西,对于大盒子来讲,叫做值中值,也可以说是值地址
大号盒子本身的地址,叫做变量地址,或者自身地址

是不是觉得有点绕,什么值中值,值地址,这地址那地址的,绕就对了,就是套娃就是玩,不上图,真的不好理解

指针风暴

int* p{ new int{666} };
int* pa{ p };

配合下图理解这两行代码,你就已经可以耍一级指针了.
指针&内存
解释:
我们向系统申请了一个以int类型方式读取值的一级指针p
p指针被赋予了一个new出来的int类型地址,地址中的值是666.

现在这个指针p可以进行三种操作.

  1. 取地址 &p(0x01)
  2. 取值 p(0x03)
  3. 取值中值 *p(666)

随后我们又申请了指针pa.
pa{p};
而只写指针名是取值操作.
所以可以翻译成下面语句
pa{0x03};

指针pa还是那三个操作

  1. 取地址 &pa(0x05)
  2. 取值 pa(0x03)
  3. 取值中值 *pa(666)

那如果我想让指针pa存的值是指针p的地址呢?
这就涉及到指针级数的问题了.
因为编译器对于非void数据读写方式的指针,都会检测地址来源的变量类型.
pa{&p}这是不合法的.
编译器检测到了p的变量,是个指针,这个指针的读写方式是int.
而pa的读写方式只是int,而不是以int指针的方式来读写.

说简单点就是,你应该让它的读写方式是个int*

int**pa{&p}; // 合法

更改后的pa指针能多取一次值了

  1. 取地址 &pa(0x05) //取自身地址
  2. 取值 pa(0x01) // 0x01是p的地址
  3. 取值中值 *pa(0x03) // 取0x01中的值,就是p存放的值,p存放的是0x03
  4. 取值中值中值 **pa(666) // 再多取一次,就是*p的值了,也就是0x03内的值.

数据类型权限和指针级数

上面更改后的pa就是二级指针.
如何判断它是几级指针,就看它指针级数的部分,有几个星星.
如果你还是有点不好理解,咱们还可以抽象一下多级指针的结构(同样适用一级指针).

int**pa{&p};
一个*代表一个地址.
**pa就是表示,我可以套两层地址.
int则表示,最后一个地址后的值,是个int类型.
应该按照int类型的方式来读写.
加上自身的地址,和pa有关联的东西,就是3个地址加1个int类型数据.

咱们做个消消乐,来方便大家理解多级指针.

pa;//这就是从 int** pa的指针级数中,拿掉了一个*
*pa;//又从int** pa的指针级数中拿掉了一个*

拿了两个级数之后,它还剩下什么了?没错,就剩下了个int.
**pa;//以int类型的方式,读取这个数据,不在是之前以指针的方式读取数据了

多级指针以此类推.
虽然说的有点啰嗦,像是幼儿园的幼师一样.
但我希望你真的能耐下心,按照这个方式去理解,这是真正不会曲解指针含义,又能让新手入门的方式了.

这种方式叫 think of cpp.
用C++视觉去拆分代码,挨个运算符的去分析其内部代表的含义.
虽然很抽象,但是不会让你之后回过头发现,自己之前理解的是错误的.
本人第一门语言就是C语言,没有任何的编程基础,也从没想过追究底层原理.
所以最开始对于指针的理解,跟着别人视频上讲的去理解,后发现,他抽象出了表面,而没有去抽象出根本.
导致我之后学习多级指针,多维数组,和所有的非线性数据结构都特别痛苦.


上面的那些,绝不是放错地方的段落.
因为这其中就有为什么应该去真正的理解指针的道理.
我们现在手里有一个地址,就可以管它叫1级指针.
这也是为什么之前,我称函数名是个1级指针的原因,因为函数名就是自身代码段的地址.

而地址中的值,理所当然就是0级指针了.(0级指针不能继续往下读地址了,再读地址就是非法寻址)
也可以说它就是个值,不过因为在汇编层面,不过是一个地址赋值给另一个地址,一个寄存器,赋值另一个寄存器.
所以将它称作0级指针是正确的.
函数名这个一级指针比较独特,它是为了让ip寄存器跳到函数代码段首地址的.
你读值,也没用,因为你最多只能读出一条汇编代码(不要想着指针++遍历代码段,这玩意不是这么遍历的)

回归正题
那么这个地址中的值,还是个地址呢?(如果你明确的知道这个值就是个地址的话)
我们将这个地址抽象层面提权为2级指针,也同样的给这个值提权为1级指针.
而这个1级指针下面的值,则是0级.

为什么我说抽象提权呢.
因为在计算机眼中,这些程序都是既定好的.
大家都是内存地址,你们是同级别的东西.
只需要读了这个地址,就应该继续读地址,然后读数据.
可是人需要去抽象的理解它.

关于指针级数的讲解,已经讲完了.
下面是数据类型权限.

比如const char* 为什么不能赋值给char[];
大家都是字符串,而且,大家轮底层也都是整数类型.
如果你对为什么数据类型不能互相赋值,还需要不同的转换感到困惑,那么下面的答案就能解决.


数据类型权限
对于数据类型赋值报错,或者说赋值冲突的问题.
为了搞明白这个问题,我们就需要知道一个知识点.
那就是数据类型,它是分权限的.
比如,可读可写,只读,这两种.
而只读,就是数据类型加const修饰过的.

拿const char*和char*举例子.

char* 可以赋值给const char*,但是const char*不能赋值给char*.
char*它是什么权限?当然是可读可写了.
而const char*是只读权限.
这俩谁权限高?肯定是可读可写的char权限高啊
权限高的可以赋值给权限低的,这叫做权限缩小赋值
权限低的不能赋值给权限高的

数据类型相同的情况下,只能进行两种赋值

  1. 权限缩小赋值
    cosnt char = char;
  2. 等同权限赋值
    char = char;

可能有的人疑问了,const char* 也可以之后赋值第二次啊.
那么建议重新回去看看指针.
const char和*得分开,这个*是个一级指针.
而const char 才是你不能改的地方.
你二次赋值的玩意,是个地址,指针又不是const修饰,当然可以赋值新的一级了.

既然讲完了权限赋值.
我想大家也顺便理解了,为什么相同数据读写方式,相同指针级数的指针,可以互相赋值而不用加上取地址.
因为这是等同权限赋值
比如
int*p {new int{}} new出来的就是个一级指针,而*p也是一级指针,它俩的赋值操作就是等同权限赋值.

取地址是抽象层面的指针提级.
int a{0}; // a这个变量名是一级地址,但是a本身使用的时候,是用值替换的,所以指针不能直接 p = a;
需要写成 p = &a;给这个变量名提级为2级指针,然后编译器又会给它降级为1级指针,这样才可以达到同级赋值

取值则是抽象层面的指针降级.
取值运算符的降级操作同理

以上,就是全部内容了


  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

八宝咸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值