基于C语言指针的一些思考

前言

最近在复习数据结构和复现数据结构算法的过程中遇到了很多困惑的点,曾经默许在脑海中的概念被一次次推翻,很羞愧当时在学习的时候没有发现这些隐晦的知识点,说明当时的自己没有认真的思考和学习,亡羊补牢,希望为时不晚。

问题的抛出

记得当时在学生时代老师教给我的第一手知识,那就是指针 = 数组变量名,从用法和意义上两者都是一样的。假设老师说的是对的,以下程序的输出结果应该是以下所示:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

void printArray(int* array, int n){
for(int i = 0; i < n; i++){
printf("array[%d] = %d\n", i, array[i]);
}
}

void main(){
int array[3] = {1, 2, 3};
printArray(array, 3);
}
1
2
3
array[0] = 1
array[1] = 2
array[2] = 3

事实上程序可以通过编译,结果也是这样的;接下来我们升级一下,让数组变成二维数组,那么依照上面的结论,数组名应该是个二级指针,修改一下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

void printArray(int** array, int n){
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
printf("array[%d][%d] = %d\n", i, j, array[i][j]);
}
}
}

void main(){
int array[3][3] = {
1, 2, 3,
4, 5, 6,
7, 8, 9
};
printArray(array, 3);
}

预想输出:

1
2
3
4
5
6
7
8
9
array[0][0] = 1
array[0][1] = 2
array[0][2] = 3
array[1][0] = 4
array[1][1] = 5
array[1][2] = 6
array[2][0] = 7
array[2][1] = 8
array[2][2] = 9

实际输出:

1
Segmentation fault

诶?为什么此刻会发生段错误呢?是不是颠覆了你此刻的人生观?好戏还在后面,假设我们将程序改成这样,我们再来看一下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

void printArray(int** array, int n){
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
printf("array[%d][%d] = %d\n", i, j, array[i][j]);
}
}
}

void main(){
int array[3][3] = {
1, 2, 3,
4, 5, 6,
7, 8, 9
};
for(int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++){
printf("array[%d][%d] = %d\n", i, j, array[i][j]);
}
}
// printArray(array, 3);
}
1
2
3
4
5
6
7
8
9
array[0][0] = 1
array[0][1] = 2
array[0][2] = 3
array[1][0] = 4
array[1][1] = 5
array[1][2] = 6
array[2][0] = 7
array[2][1] = 8
array[2][2] = 9

此刻我们发现程序现在已经有了正确的我们想要的结果,根据这些个鲜明的例子,我相信每位读完它的人都会有着满脸的疑问,抛砖引玉一下,如果我们想要弄清楚这些问题,需要搞明白以下几个点:

  • int *pint p[]中的两个p究竟是什么?
  • int **pint p[][]中的两个p究竟是什么?
  • char *p = "asdfg"; char c = p[i]char p[] = "asdfg"; char c = p[i]有区别吗?

带着这些问题,让我们探寻答案。

数组名和指针等价吗?

从刚才的例子中我们得知,数组名和指针看似是等价的,可实际上又是不等价的,在一维数组中,数组名和指针用起来的方式没有什么区别,如果将数组名赋值给一个指针变量的话都可以根据下标访问到数组中的元素并且打印出来,由此我们可以推出一个结论:

一维数组名指向数组中第一个元素,数组名本质上是一个地址

image-20201028001056553

众所周知,数组在底层是一块连续的内存,p[0]也就是取p指向的内存中存的数,p[i]也就是取p指向的内存往后偏移n * i个字节之后的地址的数,n取决于数组类型所占字节的长度,比如int是4字节,double是8字节,有点绕,看完图大概就明白了:

image-20201028003257651

到这里我们可以确认,数组名字本质上是一个指向了这个数组内存空间开端的地址,对于一级指针而言,它被赋值了数组内存空间开端的地址,利用地址偏移运算就可以取出数组中的数,所以一级指针和数组名可以无差别使用。

接下来看我们的重头戏—二级指针和二维数组,上面的小例子说明,当声明一个二维数组的时候,数组名并不是一个二级指针,在我们的印象中,一个2*2的二维数组在内存中的划分是这样的:

image-20201028003930875

我想大多数学校的老师和书本都会这么讲二维数组的内存分布,因为这样更形象化,但实则不然,真实的情况是这样的:

image-20201028004135863

二维数组数组名同样也是指向我们这块空间的起始地址,它并不是二级指针(指向指针的指针),它还是简简单单的指向了这块内存空间的起始,假设声明了一个p[m][m]这样的数组,p[i][j]指的是取p地址往后偏移n * i * m + n * j个字节的地址的数,n取决于数组类型所占字节的长度,比如int是4字节,double是8字节,是不是有点糊涂了?不急,请看图,图中p是一个2*2的二维数组:

image-20201028005653611

至此,我们终于搞明白了第二章节的程序会出现段错误,二维数组开辟内存还是一块连续的内存,并不是我们所想象的拥有几行几列的内存,二级指针指的是指针的指针,当我们将数组名作为形参传入函数中后,函数执行p[i][j]时候发生了以下操作:

image-20201028011911017

程序先获取到p的值,p的值是一个指向指针的指针,根据图示可知,取出来的值为1, 那么p[0]的值就为1,依次类推,p[1]的值为2,p[0][0]就是取以p[0]的值为地址往后偏移n * 0个字节指向的的空间(也就是地址编号为1的空间)所存的数,但是p[0][0]所指向的空间里存的是什么对我们来说是未知的,我们直接取值就会发生段错误。

在这里,我们终于可以下结论:数组名和指针不等价

总结

经过重重探索,我们终于搞明白了数组名和指针的相同点和不同点,也可以回答在第二章节中提出的几个问题:

  • int *pint p[]中的两个p究竟是什么?

*p是一个指针变量,p是一个数组,指向了一块内存区域

  • int **pint p[][]中的两个p究竟是什么?

**p是一个二级指针变量,p是一个数组,指向了一块内存区域

  • char *p = "asdfg"; char c = p[i]char p[] = "asdfg"; char c = p[i]有区别吗?

在C语言编译器中,将数组名视为一种特殊的类型,当我们去定义了char p[10]之后,p就代表了一个长度为10 char类型的数组,p的值在编译阶段就存在,然而char *p的话编译器会认为这一个变量,一个保存了char类型的变量地址的指针,编译阶段只保存的p的地址,p的值必须得等编译之后才能进行取值

如果是char *p = "asdfg"; char c = p[i]这种情况下,编译器在编译阶段会先取p的值,然后会根据地址的偏移取数;

如果是char p[] = "asdfg"; char c = p[i]这种情况下,编译器会直接将p进行偏移取数;

这也是数组名和指针变量最大的不同。

以上便是我本次对于指针的思考和感悟,这次的经历让我对C语言的指针有了更加深刻的理解,如有谬误,欢迎各位大佬进行讨论。

我的微信公众号

参考书籍:C专家编程