从零开始的逆向之旅~数组与指针(地址)篇
2026/9/16 19:12:05 网站建设 项目流程

如果无特殊提醒,我将会默认读者熟悉C语言或C++的基础语法,还请注意一下。

另:本文及之后对代码的讨论只限于c++,编译方式工具链使用minGw与cmake,还请注意。


数组——C语言或是C++的一种存储集中型数据的方案,简单来说,数组是存储数据的最基础的方式。作为最简单的数据存储方式之一,设计者并没有对数组进行高级的封装,包括指针在内,二者几乎是程序员与内存交互的唯一途径。他们很简单,但是有点过于简单了,导致使用的时候相当的蹩脚——不过没有关系,俗话说万事开头难。

首先看数组的定义

int a[10];

我们知道这个语句简单的定义了一个名为a的数组,长度为10。为了进行更深的理解,我们可以对这个式子进行简单的拆解

noptr-declarator[expr (可选)]attr (可选)

其中,noptr-declarator可以是任何有效的声明符,例如上面代码中的int(整形数组),或者float(浮点数组)再或者是char(字符数组)之类。

expr可以是任何的常量表达式(在C++14之前)或者是类型为std::size_t的转换常量表达式(C++14之后)

attr为C++11开始使用的特性,引入用于类型、对象、代码等的实现定义属性。这不是我们今天讨论的范围。

我们回到expr上面,上面说过这是个常量表达式或是一个类型为std::size_t的转换常量表达式——我们可以换个说法,实际上常量表达式就是可以在编译时求值的表达式,更简单的说,在C++中,形如

int n; cin >> n; int arr[n];

这样的代码是不被C++标准所明确允许的,如此声明变长数组,尽管很多编译器对其进行了适配,仍属于非标准行为,C++中应该用new/delete来声明边长数组。

我们用T来形容声明符,形如T a[N]的数组,该对象由N个T类型的对象组成,分别为a[0],a[1]......a[N-1]。数组可以为任何的基本数据类型(当然除了void)来提供载体,除了基本类型,其他包括类、指针的对象,只要是完整的,都可以使用数组进行存储。

我们经常使用 a[n]来访问一个数组,其中[]被称为成员访问运算符,在程序中等价于*(a + n)。什么意思呢?a在这里被声明为数组对象,在运算中它经常被隐式转换为指针类型,指向这个数组的起始位置,所以上式可以被解读为取a数组起始位置之后第n个元素。

为什么可以这么取?因为程序在给数组分配空间时,经常分配一块连续的区域给数组使用,知道了数组在内存中是连续的,我们就可以有如下的操作方式:

int main() { int a[4] = {1, 2, 3, 4}; int* p = &a[2]; std::cout << p[1] << p[-1] << 1[p] << (-1)[p] << '\n'; }

输出为4242,我们来逐行解析一下。

int a[4] = {1, 2, 3, 4};

声明了一个元素为1,2,3,4的数组,很简单。

int* p = &a[2];

声明了一个名为p的指针变量,指向了数组a的第三个元素的地址(数组是从0起的,a[0]的元素就是1)。

std::cout << p[1] << p[-1] << 1[p] << (-1)[p] << '\n';

接下来是最重要的输出环节,我们刚说过p是指向数组a第三个元素地址的指针变量,而p[1]等价于*(p + 1),我们不妨简单做一下运算,既然int* p = &a[2],我们进行换算,有:

p = a + 2

其中a被隐式转换称指针类型变量,所以p[1]就是*(p + 1)=*(a+1+2)=*(a+3)=a[3],也就是访问a的第4个元素,输出的就是4,同样的,如果是p[-1],我们很容易得出其等价于a[1],也就是a的第二个元素,输出的是2。

理解了数组的本质,后面的1[p]也很好理解了,因为加法有交换律,所以1[p] = *(1 + p) = *(p + 1) = p[1]。所以最后输出4242就很好理解了,因为p[1]和1[p]并没有任何区别(除了会让代码变得很难看)。


好了接下来我们来看指针。

指针的声明方式有两种,一种是指针声明符,一种是指针声明符,另一种是指向成员的指针声明符,其中后者不在我们今天的讨论范围内,我们只讨论前者就好。

指针声明符的定义如下

*attr (可选) cv (可选) declarator

我们不去深究这定义的格式,考虑形如S* D;的声明,其中D被声明为指向S类型的指针,S类型可以是任意声明说明符,包括但不限于int,char,或是string(需加string文件头)等类也可以。

所有的指针都可以是如下四个类型之一

  • 指向对象或函数的指针(在这种情况下,该指针被称为指向该对象或函数),或
  • 指向对象末尾之后的指针,或
  • 该类型的空指针值,或
  • 无效指针值

当然,这四种类型中每一个都值得单开一个文章进行介绍,我们今天只讨论指向对象的指针就好。虽然有些晚,我们先介绍一下跟指针相关的两种运算符,&和*。

&在C++中被称为是取地址符,是成员访问运算符之一,它的作用就是对紧跟其后面的成员(我们在本文章仅讨论对象,实际上对函数等也通用)取地址,形如

int a; int* b = &a;

其中b的值就是a的地址,b被称为指向a的指针

再往深一点进行探讨,何为地址?

在现代计算机系统中,一个程序想要运行,就必须将其导入进内存中,而所谓地址就是程序在内存中的位置。操作系统通过地址来寻找到各变量与函数的内容,以供程序使用,地址是计算机中非常底层且重要的知识,我们之后会详细解释地址的深层知识及其底层实现与运用。

理解了地址我们就可以理解指针,我们常说地址≈指针,实际上,指针存储的是指向内容的地址,当我们讨论一个指针的时候,通常将指针视为两种内容的集合。

其中之一便是地址,这是我们刚才就说过的。另一种就是指针所指向内容的类型。

什么意思呢?考虑以下代码:

int a = 1; char* p1 = &a; int* p2 = &a;

可以看到编译器给我们报错——不能用int* 类变量来初始化char*变量,我们对它进行强制类型转换,变成如下代码:

int a = 64; //ASCII码表中的“A” char* p1 = (char*)&a; int* p2 = &a;

同时输出p1,p2,我们可以看到这两个指针变量指向了同一个值,但是编译器却阻止我们把两个值进行比较,报错为“不能将==运用于int*与char*”。

因为指针变量除了存储地址,它还存储了其指向对象的类型,所以编译器阻止了我们对其的比较。

接下来我们讨论一下解引用符*。有了前面的知识储备,我们很容易知道形如

cout << *p2 << endl;

这一行代码意味着什么,那么,

cout << *p1 << endl;

这段呢?

我们知道*是对地址的解引用,这是什么意思?地址是指引计算机找到对应值的“地图”,指针还存储着指向变量的类型,这在上面的cout里面是什么意思?很简单——既然地址是地图,那么指针存储的数据类型就是找到的“宝藏”的用法。

用更通俗的话来讲,操作系统依照代码的请求,找到了处于某个地址的一个变量值,而指针的存储类型就决定着操作系统该把依照地址找到的这一堆数字当成什么。

现在按照上面的cout输出一遍,可以看到两个值——分别是A(p2)和65(p1),所以我们知道了在c系这类强类型语言中,指针变量具有类型并且不允许互相换算的原因了——操作系统需要以指针的类型为依据解读沿地址寻找到的值。


注:本文的C++规范参照以及部分代码来自https://cppreference.cn/

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询