指针的重要性,是毋庸置疑的.
有很多时候你都需要对内存进行管理,没有指针,就有受苦的时候了.
没有指针,依托节点数据结构铸造出来的数据类型,就是无稽之谈.
总不可能为了实现这些非线性数据结构,去写内联汇编吧.
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
其实这个标题并不准确,因为上面也有很多关于指针的原理.
但是有些东西吧,就是和原理有牵连,所以只好就在上面一块讲了.
一块内存可以被很多指针指向
而指针本身,也有属于自己的内存空间.
它的工作原理就是在计算机申请一块内存空间,然后这个内存空间放的是另一个内存空间的地址.
我们可以通过指针来对其里面的内存空间进行有权限(读写)操作.
内存地址是盒子编号,盒子里面放的东西是值.
这个盒子里面可能又放了一个盒子.这个小盒子,同样叫做值.
然后这个小盒子里的东西,对于大盒子来讲,叫做值中值,也可以说是值地址
大号盒子本身的地址,叫做变量地址,或者自身地址
是不是觉得有点绕,什么值中值,值地址,这地址那地址的,绕就对了,就是套娃就是玩,不上图,真的不好理解
int* p{ new int{666} };
int* pa{ p };
配合下图理解这两行代码,你就已经可以耍一级指针了.
解释:
我们向系统申请了一个以int类型方式读取值的一级指针p
p指针被赋予了一个new出来的int类型地址,地址中的值是666.
现在这个指针p可以进行三种操作.
随后我们又申请了指针pa.
pa{p};
而只写指针名是取值操作.
所以可以翻译成下面语句
pa{0x03};
指针pa还是那三个操作
那如果我想让指针pa存的值是指针p的地址呢?
这就涉及到指针级数的问题了.
因为编译器对于非void数据读写方式的指针,都会检测地址来源的变量类型.
pa{&p}这是不合法的.
编译器检测到了p的变量,是个指针,这个指针的读写方式是int.
而pa的读写方式只是int,而不是以int指针的方式来读写.
说简单点就是,你应该让它的读写方式是个int*
int**pa{&p}; // 合法
更改后的pa指针能多取一次值了
上面更改后的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权限高啊
权限高的可以赋值给权限低的,这叫做权限缩小赋值
而权限低的不能赋值给权限高的
数据类型相同的情况下,只能进行两种赋值
可能有的人疑问了,const char* 也可以之后赋值第二次啊.
那么建议重新回去看看指针.
const char和*得分开,这个*是个一级指针.
而const char 才是你不能改的地方.
你二次赋值的玩意,是个地址,指针又不是const修饰,当然可以赋值新的一级了.
既然讲完了权限赋值.
我想大家也顺便理解了,为什么相同数据读写方式,相同指针级数的指针,可以互相赋值而不用加上取地址.
因为这是等同权限赋值
比如
int*p {new int{}} new出来的就是个一级指针,而*p也是一级指针,它俩的赋值操作就是等同权限赋值.
取地址是抽象层面的指针提级.
int a{0}; // a这个变量名是一级地址,但是a本身使用的时候,是用值替换的,所以指针不能直接 p = a;
需要写成 p = &a;给这个变量名提级为2级指针,然后编译器又会给它降级为1级指针,这样才可以达到同级赋值
取值则是抽象层面的指针降级.
取值运算符的降级操作同理
以上,就是全部内容了