指针概述
内存基本原理
内存示意图:
1bit
↓
□□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□
↑ ↑
0x00000000|0x00000001
内存中最小的物理单位是【比特(bit)】,1个比特可以存储两种状态,即低电平与高电平,用数字模拟也就是0和1。因为1个bit只能存储两种状态,无法满足更大的数据存储的需求,所以计算机将8个bit作为一组存储单位,这样的存储单位1个就可以存储2的8次方(256)种状态,这样的存储单位就叫做【字节(byte)】,也是计算机内存中最小最基本的存储单位。C++中最小的数据类型char占用一个字节,计算机系统里的内存4G指的是4G个字节 , 内存地址也是以字节为单位而不是以bit为单位。
指针
指针是用来存储变量内存地址的变量。
□□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□
↑ ↑
0x00000000|0x00000001
要操作内存,需要知道两个要素:
(1)要操作的内存地址 (从哪开始)
(2)要操作的内存大小 (到哪结束)
指针的语法
数据类型* 变量名称
int* pStudent{};
pStudent为一个int类型的指针
数据类型解决要操作内存的大小问题,int类型表示该指针要操作int大小的内存也就4个字节。pStudent则存储了内存地址。
取址运算符 &
利用取之运算符可以获得一个变量的内存地址
int a{ 5000 };
int* pa{&a};
std::cout << pa<<char(10);
008FF8F8
间接运算符 *
利用间接运算符可以操作指针所表示的内存。
int a{ 5000 };
int* pa{&a};
*pa = 5001; //写入
std::cout << *pa<<char(10); //读取
5001
指针与数组
int studentId[5]{ 1001,1002,1003,1004,1005 };
int* ptrStudentId[5];
for (int x = 0; x < 5; x++)
{
ptrStudentId[x] = &studentId[x];
std::cout << ptrStudentId[x] <<":"<<*ptrStudentId[x]<< char(10);
}
0095F6EC:1001
0095F6F0:1002
0095F6F4:1003
0095F6F8:1004
0095F6FC:1005
指针的大小
既然指针是一个变量,那么它就会占用内存空间,指针的大小是多少呢?
首先,指针类型要大到能够描述操作系统所支持的所有内存地址,而操作系统是根据CPU的寻址能力来设计的,32位操作系统与32位CPU相匹配,32位CPU的寻址能力为2的32次方,也就是4,294,967,296,为 unsigned int的大小,也就是4个字节。所以,32位操作系统的指针需占用4个字节的内存。64位CPU的寻址能力为2的64次方,也就是18,446,744,073,709,551,615,为unsigned long int的大小,也就是8个字节,所以64位操作系统的需要占用8个字节的内存。
寻址能力与内存大小
拿32位CPU为例,32位CPU是指CPU的总线的数量为32根,CPU运行时每次最多可以通过32根线来传输数据,而每根线是通过高低电来传输电平信号,也就是0和1,那么每根线2种状态,32根线,那么最多就可以模拟出2的32次方种状态,也就是4,294,967,296种状态,用16进制表示就是FFFF FFFF,也就是说,32位CPU一次最多也就能访问这么大的内存。4,294,967,296种状态用4个字节就能描述,那么32位操作系统的指针的大小用4字节也就够了。
指针类型转换
指针的类型是可以强制转换的。
unsigned ua{ 9999 };
int* ptr{ (int*)&ua };
std::cout << *ptr << char(10);
*ptr = -1;
std::cout << *ptr << char(10);
std::cout << ua << char(10);
9999
-1
4294967295
上例代码中讲usigned类型指针&ua转换成了int类型的指针ptr,然后修改了内存中的内容为-1,尽管*ptr与ua都是获取到同一块内存区域的内容(也就是-1),但是由于类型不同,描述内存内容的方式也就不同,输出的结果也就不同。
*ptr = -1;这行代码是将int类型的-1存储到了内存中,int的-1在内存中存储为FFFFFFFF(-1=1的补码+1),当使用unsigned类型描述FFFFFFFF时则会变成unsigned类型的最大值,也就是结果中所打印的4294967295。
指针运算之自增与自减
指针的运算基于指针的类型。
int a[5]{ 1001,1002,1003,1004,1005 };
int* ptr{ &a[0] };
std::cout << ptr++ << char(10);//先显示ptr,再++
std::cout << ptr-- << char(10);//先显示ptr,再--
std::cout << ptr << char(10);
00CFFA94
00CFFA98
00CFFA94
上例所示,指针加n等于将指针增加n个该指针类型的大小。
多级指针
代码 | 变量 | 值 | 内存地址 |
---|---|---|---|
int a{500} | a | 500 | 0x50000 |
int* ptr{&a} | ptr | 0x50000 | 0x50100 |
int** pptr{&ptr} | pptr | 0x50100 | 0x50200 |
int*** ppptr{&pptr} | ppptr | 0x50200 | 0x50300 |
… | … | … | … |
int a[5]{ 1001,1002,1003,1004,1005 };
int* ptr{ &a[0] };
int** pptr{ &ptr };
std::cout << *ptr << char(10);
std::cout << ptr << char(10);
std::cout << pptr << char(10);
*pptr = &a[1];
std::cout << *ptr << char(10);
1001
0058FDEC
0058FDE0
1002
上例中,ptr是a[0]的指针,pptr是ptr的指针,将pptr的值(也就是ptr所存储的指针)变为a[1]的指针后,ptr就变成了a[1]的指针。这样,就可以在不操作ptr的情况下,完成对ptr的修改。
一些道理简单却容易搞混的概念:
常量指针
常量指针就是指向一个常量的内存地址的指针,常量指针中,不能对其指向的内存进行改变,但是指针指向的地址可以改变。
const int a{ 1000 };
const int b{ 2000 };
const int* ptrA{ &a };
std::cout << *ptrA << char(10);
ptrA = &b;
std::cout << *ptrA<<char(10);
//*ptrA=500; 编译出错
1000
2000
指针常量
指针常量是指这个指针变量为一个常量,一旦初始化就不可以再指向其他内存地址,但它指向的内存里呢数据可以读写。
int a{ 1000 }
int b{ 2000 };
int* const ptrA{ &a };
std::cout << *ptrA << char(10);
//ptr = &b; 编译出错
*ptrA = 9999;
std::cout << a << char(10);
1000
9999
指向常量的常量指针
这种类型的指针,基本能修改指针的地址,也不能修改指针所指向的内存的数据。
const int a{ 1000 };
const int b{ 2000 };
const int* const ptrA{ &a };
std::cout << *ptrA << char(10);
//ptr = &b; 编译出错
//*ptrA = 500;//编译出错
1000
指针与数组
数组的底层逻辑
数组的底层实现是利用了指针,指针和数组是同一个方法的不同表达,而数组名本身是数组第0个元素的指针,数组元素是这个指针按照一定偏移量(下标*数组类型) 偏移后所对应的指针的内存内容。
下标取值的底层逻辑
a[x]的底层逻辑是将指针a向右偏移x个数组类型的长度,然后取值。
int a[3]{1,2,3};
std::cout<<a[2]<<char(10);
2
上例中a[2]的值的计算方式为: *(&a[0] + 2),或者 *(a+2);
int a[5]{1000,1001,1002,1003,1004};
int* ptrA{ a};
std::cout << a << char(10);
std::cout << &a[0] << char(10);
std::cout << ptrA << char(10);
std::cout << a[3] << char(10);
std::cout << ptrA[3] << char(10);
004CFC98
004CFC98
004CFC98
1003
1003
指针与二维数组
int a[2][4]{
{1001,1002,1003,1004},
{2001,2002,2003,2004}
};
int(*ptra)[4]{a};
std::cout << ptra << char(10);
std::cout << a << char(10);
std::cout << a[0] << char(10);
std::cout << ptra[0] << char(10);
006FFD7C
006FFD7C
006FFD7C
006FFD7C
既然是二维数组,那就先把a[2][4]拆分描述成两个维度,第一维度是4,第二个维度是2。
那么,
ptra是一个指向了a的指针,该指针类型为int[5]。
a表示二维数组a的内存地址,也就是a[2][4]的首个元素地址,即a[0][0]的地址。
a[0]表示在第二维度中的第0个元素,那就是{1001,10002,1003,1004}这组数据的内存地址。也就是{1001}的内存地址。
ptra[0]与a[0]相同,a的实质就是ptra。