本篇博客所指的所有内存均不区分高级缓存、内存、虚拟内存

本篇博客也不会单独讲解数组,阅读之前请确保自己已经掌握了数组的基本内容

内存

这里只是对内存简单并片面的进行了简单的介绍,更多内容还请自行搜索

  说到指针,肯定无法避免的和内存扯上关系。从硬件层面来说,内存是用来临时(断电丢失数据)存储程序运行时数据的高速存储设备。从软件层面来说,内存相当于一个容量巨大的一维数组,下标从0开始,依次向后排列,这个下标也就是我们经常说的“内存地址”。

  需要注意的是,实际开发中(仅限于win10平台,win从什么版本开始这个设计和其它平台的我不清楚)所有程序的内存地址都是从0开始的,这是因为操作系统为了安全性考虑没有将真实的内存地址暴露给程序,程序访问到的是其实是一个虚拟的内存地址,程序的内存地址表示的是该内存在操作系统给程序分配的内存空间中的位置。

指针

本质

  指针的声明方法如下(格式:[类型] *[变量名]):

1
2
3
int* i;
double* d;
void* v;

  读者可能会好奇,为什么可以声明一个类型为void的变量?这就涉及到指针的本质了。指针(不论什么类型的指针)实际上是一个整形数据,其中存储的是它指向的量的内存地址。void *v表示指向一个数据类型未知的量的内存地址。

空指针

  C/C++中提供了一个值:NULL(C++中还提供了nullptr),该值用于表示当前指针为空值,即没有指向任意有效的内存。一般情况下,当当前指针不可用时,推荐手动将其赋值为NULL(或nullptr),以防后面不小心再次使用。

  nullptr是C++中的一个关键字,与NULL不同的是,NULL其实是int类型,值为0,而nullptr并不是一个值。比如:

1
2
int arg0 = NULL;    //1
int arg1 = nullptr; //2

  其中第一行代码是可以通过编译的,而第二行的则不可以。

指针的声明方式

  很多人可能会疑惑下面两种写法哪一种正确:

1
2
int *arg0;
int* arg1;

  这两种声明方式放在程序中都可以正常编译并运行,但是假如我们要声明三个指针,下面两种方法哪种正确呢?

1
2
int *a0, *a1, *a2;  //方法一
int* b0, b1, b2; //方法二

  现在情况发生了变化,其中一种写法会导致编译错误(指使用时报错,而非声明时报错)或运行错误(如果编译通过了的话),读者可以先自行尝试一下。

  实际上这两种写法到如今仍然有争议,以至于出现了第三种写法:(一般使用前两种就行)

1
int * arg2;

  细心的读者可能会发现,连续声明多个指针的时候,按照上文中方法二的方式会出现问题,因为方法二其实是声明了一个指针:b0和两个intb1b2

  说到最后,其实也没有一个确切的结论,个人认为这可能是历史遗留问题或者是设计缺陷,两种指针的声明方式读者选其一就行,不要混用。另外,不要像方法一一样在一行内连续声明多个指针,因为这很容易造成意想不到的问题。

指针加法

  指针的加法和数字的加法不同,我们知道,不同类型的数据的长度是不一样的,以64位电脑为例,int32位/4字节char8位/1字节。指针的加法会根据类型的不同而变化,比如对于int *a0a0 + 1返回的实际上是a0所指向的地址再加4的结果。简单说就是指针+1的结果是当前地址+数据类型长度的结果。

  由此我们可以得到如下公式:point + k -> point + (k * sizeof(type)),其中point是指针对象,k是要加的数字,type是数据类型。需要注意的是,这个转换编译器已经帮我们完成,我们使用的时候只需要用左边的写法就可以了。

指针和数组

一维(一级)

数组

  我们写出如下代码:

1
2
int array[10];
int *arrayPoint = array;

  可以知道,我们通过arrayPoint来访问数组是完全没有问题的,因为标准规定,数组名存储的是数组中第一个元素的内存地址。所以我们可以把数组看作一种另类的指针(注意是另类的指针而并不是指针)。调用函数并传递数组进去也不会把数组复制一遍,而是传递数组中第一个元素的地址。

指针

  那么我们如何用指针来表示数组呢?如下面的代码:

1
int *array = malloc(10 * sizeof(int));

  malloc在堆分配了一个长度为10 * sizeof(int)的内存空间,并且返回这段内存的起始位置的地址。由此可知,我们将数组的地址存储到了array中,实际的内容分配到了堆中。结构如下:

指针

二维(二级)

  有了上面的结论,我们可以写下这样子的代码:

1
2
int array[10][10];
int **arrayPoint = array;

  经过测试可以发现,这段代码无法通过编译,为什么呢?因为数组名只是一种另类的指针,而非指针,它和指针其实完全不是一种东西。

指针

  声明指针形式的二维数组的方法如下:

1
2
3
4
int **arrayPoint = malloc(3 * sizeof(int*));
for (int i = 0; i < 3; ++i) {
arrayPoint[i] = malloc(3 * sizeof(int));
}

  其内存结构如下:

内存结构

  这里解释一下为什么会这样。首先,我们在栈中存储了数组的地址。然后为数组分配了一个内存空间,这里要注意,我们分配空间的时候先使用malloc分配了一个长度为3 * sizeof(int*)的空间,随后再遍历这个一维数组,继续分配二维空间。可以理解为我们创建了四个数组,其中一个用于存储剩余数组的起始内存地址。

数组

1
2
int arr1[9];
int arr2[3][3];

  我们来看一下数组的内存结构(数组声明在函数中且不是static就存在栈中,否则在堆中):

二维数组内存结构

该图来自网络,侵删

小结

  对比内存结构我们发现,一维数组可以通过指针进行操作完全是因为两者的内存结构“凑巧”一样而已,而到了二维(或更高)层次后,两者内存结构的区别便凸显了出来,所以显然是不能够共用的。

  那么如果我非要用指针的形式表示二维数组应该怎么声明呢?

1
2
3
4
int array[10][10];

int *p1[10] = array; //1
int (*p2)[10] = array; //2

  我们先来分析一下这两种写法的区别:

*[] - 指针数组

  指针数组实际上是一个数组,其中存储了一些指针的值,内存结构如下:

指针数组

(*)[] - 数组指针

  数组指针实际上是一个指针,指针指向数组所在的内存地址,内存结构如下:

指针数组


  现在,已知上面两种写法其中一种是正确的,读者可以尝试根据这两种内存结构推理一下哪一种是可以通过编译的。

  答案是第二种,为什么呢?我们将二维数组带入进去可以发现,第二种写法只是存储了数组中第一个元素的地址,这和二维数组的内存结构是一致的,所以可以正确运行。而第一种写法其实其内存结构和二级指针构成的数组大差不差。


参考资料

C语言中的二级指针和二维数组问题

C语言中指针和数组

数组指针和指针数组的区别