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

目录

    • C/C++指针存在的必要性
    • 内存和指针原理的简易认知
    • 指针的理解
    • 指针相关运算符
    • 数据指针和函数指针的声明方式
    • 指针原理
    • **指针风暴**
    • 数据类型权限和指针级数


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

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

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

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


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

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

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

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

存储单位(位) 对应关系 位数量(bit)
位(bit) 1 bit 1
字节(byte) 8 bit 8
千字节(KB) 1024 byte 8192
兆字节(MB) 1024 KB 8,388,608
千兆节(GB) 1024 MB 8,589,934,592
太兆节(TB) 1024 GB 8,796,093,022,208
拍字节(PB) 1024 TB 9,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


指针原理

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

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

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

指针风暴

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

配合下图理解这两行代码,你就已经可以耍一级指针了.
[C/C++] 指针的原理和对指针的运用及理解(包括函数指针和多级指针)_第2张图片
解释:
我们向系统申请了一个以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级指针,这样才可以达到同级赋值

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

以上,就是全部内容了


你可能感兴趣的:(HelloWorld,指针,c++,内存管理)