Skip to content

01.指针和数组

从汇编看指针和数组的关系

NOTE

知识点:汇编指令的学习

在 vs 中输入以下源代码:

c++
#include <iostream>

int main()
{
    int a[5];

    int* ptrA{ &a[0] };
    *ptrA = 5;

    a[0] = 5;
    a[1] = 5;
    a[2] = 5;
}

然后在 int* ptrA{ &a[0] }; 处打上断点,点击运行,然后点击 调试 -> 窗口 -> 反汇编 ,找到汇编代码,如下:

assembly
    int a[5];

    int* ptrA{ &a[0] };
00007FF736121E7E  mov         eax,4  
00007FF736121E83  imul        rax,rax,0  
00007FF736121E87  lea         rax,a[rax]  
00007FF736121E8C  mov         qword ptr [ptrA],rax  
    *ptrA = 5;
00007FF736121E90  mov         rax,qword ptr [ptrA]  
00007FF736121E94  mov         dword ptr [rax],5  

    a[0] = 5;
00007FF736121E9A  mov         eax,4  
00007FF736121E9F  imul        rax,rax,0  
00007FF736121EA3  mov         dword ptr a[rax],5  
    a[1] = 5;
00007FF736121EAB  mov         eax,4  
00007FF736121EB0  imul        rax,rax,1  
00007FF736121EB4  mov         dword ptr a[rax],5  
    a[2] = 5;
00007FF736121EBC  mov         eax,4  
00007FF736121EC1  imul        rax,rax,2  
00007FF736121EC5  mov         dword ptr a[rax],5

我们关注从 int* ptrA{ &a[0] }; 开始的汇编代码。

上述用到的汇编指令的总结

汇编指令格式支持的传输硬件和传输方向等
movmov 目标, 源目标和源可以是寄存器、内存、立即数,但不能同时为内存。
方向是从源到目标
imulimul 目标, 源, 乘数目标和源通常是寄存器,乘数可以是立即数或寄存器。
方向是源寄存器被乘以乘数,存入目标寄存器
lealea 目标, [基址 + 偏移]目标是寄存器,源是内存地址的计算表达式(但不会访问内存)。
方向是计算出的地址存入目标寄存器

以下是上述汇编代码的执行过程(从左至右):

rax 和 eax 指的都是同一个寄存器,其中 rax 表示该寄存器的全部位数,eax 表示该寄存器的低 32 bit 。

汇编

结论:

  • 在数组中,aa[0] 是等价的,a 就是数组 a 的起始地址,也就是 a[0] 的地址

    验证:

    c++
    std::cout << a << std::endl;
    std::cout << &a[0] << std::endl;

    输出:

    00000080C9DAF568
    00000080C9DAF568
  • 我们说 a[x] 时,将 ax 分开来看,就是在 a 的基础上,向右偏移 x 个单位。如 a[2] ,表示在 a 的基础上,向右偏移 2 个单位,如果是 int 类型,就是 2 * 4 = 8 个 bit 。

通过以上结论,可以继续得出结论:

  1. 在数组的指针初始化时,可以使用 a 进行初始化而不是 &a[0]int* ptrA{ a };
  2. 数组 a 本身就是一个指针,数组拥有的方法,如 a[2] 访问数组 a 中的第 3 个元素,指针也可以:ptrA[2]
  3. C/C++ 中的数组是使用指针来实现的

指针和数组的大小

使用 sizeof 查看指针和数组的大小:

c++
std::cout << sizeof(a) << std::endl;
std::cout << sizeof(ptrA) << std::endl;

输出:

20
8

结论:

虽然 a 本质上也是指针,但使用 sizeof 方法求元素的大小时仍会利用一些方法,使得“明面”上的数组 a 求大小时给出的为整个数组的大小。

多维数组和指针的关系

同上,多维数组也是通过指针来实现的。

c++
// 多维数组
int test[2][5]{
    {1001,1002,1003,1004,1005},
    {2001,2002,2003,2004,2005}
};

注意数组指针和指针数组的区别,指针数组是数组,其中每个元素为一个指针;数组指针为指针,是一个专门用来指向数组的指针:

c++
// 数组指针
int* ptestA[5];  // 表示 5 个 int 类型的指针,是指针数组
int(*ptest)[5] { test };  // 表示一个数组指针,其逻辑是可以处理 5 个一组的数组,因此可以直接初始化为 test

关键点在于 int(*ptest)[5] { test }; 中的 [5] ,其表示声明该指针是专门用来处理 5 个一组的单维数组的指针。

这种声明指针的方法,指针一定是指向多维数组中最外层的数组类型,或说专门处理最外层数组类型(维度)的指针,后文的 [2][3][4] 的 3 维数组,对应的指针就是指向的 [3][4] 模式的二维数组。

使用该指针来访问多维数组:

c++
std::cout << ptest[1][4] << std::endl;  // 输出 2005

由于数组的本质仍是指针,上述无法直接将数组指针直接赋值为普通的指针原因在于数组指针除了指针的功能外还具有其他的一些特性,如可以表示当前数组模式为 5 个一组,因此可以通过强制类型转换,将数组指针强制转换为一个普通指针,从而进行赋值:

c++
int* pnormaltest{ (int*)test };  // 因为数组的本质是指针,多维数组本身比单维数组多出一些特性,如表示了 5 个一行等,因此不能直接初始化为普通指针,但可通过强制类型转换赋值给普通的指针
std::cout << pnormaltest[9] << std::endl;  // 输出 2005

多维数组指针的大小:

c++
std::cout << sizeof(ptest) << std::endl;  // 输出 8

注意多维数组指针的自增大小,是增加的组数的大小,而不是数组最内部元素的大小:

c++
std::cout << "ptest:\t" << ptest << std::endl;
ptest++;
std::cout << "ptest:\t" << ptest << std::endl;
// 差值是 20D,除以 int 大小 4 为 5,即 5 个一组

根据之前单维数组的访问机制,a[0] 可以分开来看,a 表示数组起始地址,0 表示偏移量,因此,多维数组也可以分开来看,a[1][2] 中,将 a[1][2] 分开来看,a[1] 表示一个基址,2 表示偏移量:

c++
// 查看 ptest 一层的数值
ptest--;  // 恢复 ptest 的初始位置
std::cout << ptest[1] << std::endl;
std::cout << test[1] << std::endl;  // 两者相等

三维数组,这里仅作代码和注释的展示:

c++
// 三维数组
int test2[2][3][4]
{
	{
		{1,2,3,4},{5,6,7,8},{9,10,11,12}
	},
	{
		{13,14,15,16},{17,18,19,20},{21,22,23,24}
	},
};
int (*ptest2)[3][4]{ test2 };  // 表示指向的是 3 行 4 列的二维数组类型的指针
std::cout << "ptest2:\t" << ptest2 << std::endl;
ptest2++;  // 因此自增就是自增了一个 3 行 4 列的二维数组的大小,这里为 4 * (3 * 4) = 48 bit
std::cout << "ptest2:\t" << ptest2 << std::endl;  // 差值为 48

本节完整代码

c++
#include <iostream>

int main()
{
	int a[5];

	int* ptrA{ &a[0] };
	*ptrA = 5;

	a[0] = 5;
	a[1] = 5;
	a[2] = 5;

	std::cout << a << std::endl;
	std::cout << &a[0] << std::endl;

	// 使用 sizeof 查看指针和数组的大小
	std::cout << sizeof(a) << std::endl;
	std::cout << sizeof(ptrA) << std::endl;

	// 多维数组
	int test[2][5]{
		{1001,1002,1003,1004,1005},
		{2001,2002,2003,2004,2005}
	};
	// 数组指针
	int* ptestA[5];  // 表示 5 个 int 类型的指针,是指针数组
	int(*ptest)[5] { test };  // 表示一个数组指针,其逻辑是可以处理 5 个一组的数组,因此可以直接初始化为 test
	// int* ptest{ test };  // ptest 只是一个普通的指针,无法直接处理 5 个一组的数组,因此不能初始化为 test
	std::cout << ptest[1][4] << std::endl;  // 输出 2005

	// 可以进行强制类型转换,将 test 强制转换为一个普通的指针,从而赋值给普通指针
	int* pnormaltest{ (int*)test };  // 因为数组的本质是指针,多维数组本身比单维数组多出一些特性,如表示了 5 个一行等,因此不能直接初始化为普通指针,但可通过强制类型转换赋值给普通的指针
	std::cout << pnormaltest[9] << std::endl;  // 输出 2005
	// 进一步验证多维数组本质仍为指针,所谓的多维只是人为构造的逻辑上的多维“有序”

	// 查看“多维(自带逻辑)”指针的大小
	std::cout << sizeof(ptest) << std::endl;

	// “多维”指针自增的大小
	std::cout << "ptest:\t" << ptest << std::endl;
	ptest++;
	std::cout << "ptest:\t" << ptest << std::endl;
	// 差值是 20D,除以 int 大小 4 为 5,即 5 个一组

	/*
	根据之前单维数组的访问机制,a[0] 可以分开来看,a 表示数组起始地址,0 表示偏移量,
	因此,多维数组也可以分开来看,a[1][2] 中,将 a[1] 和 [2] 分开来看,a[1] 表示一个基址,2 表示偏移量
	*/
	// 查看 ptest 一层的数值
	ptest--;  // 恢复 ptest 的初始位置
	std::cout << ptest[1] << std::endl;
	std::cout << test[1] << std::endl;  // 两者相等

	// 三维数组
	int test2[2][3][4]
	{
		{
			{1,2,3,4},{5,6,7,8},{9,10,11,12}
		},
		{
			{13,14,15,16},{17,18,19,20},{21,22,23,24}
		},
	};
	int (*ptest2)[3][4]{ test2 };  // 表示指向的是 3 行 4 列的二维数组类型的指针
	std::cout << "ptest2:\t" << ptest2 << std::endl;
	ptest2++;  // 因此自增就是自增了一个 3 行 4 列的二维数组的大小,这里为 4 * (3 * 4) = 48 bit
	std::cout << "ptest2:\t" << ptest2 << std::endl;  // 差值为 48
}

Last updated: