C语言语法详解之指针(四)从入门到入土
1.0数组的本质
1.1数组名的理解
我们先来看这样一串代码
int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0]我们这里使用了&arr[0]的方式取出了数组中首个元素的地址,但是实际上数组名本身就是地址,只不过这里使用了取地址的方式方便理解。那么我们不禁要思考一个问题,一个数组内部有那么多的元素,如果只写出一个数组名不标注序号,出现的地址是哪一个元素的呢,我们来做一个测试。
//测试环境:X86 #include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); return 0; }输出结果如下
这时候我们会发现单独一个数组名竟然和首元素地址的输出结果是一样的,那么显然,当我们单独打出一个数组名的时候得出的结果就只会与其首元素地址相同,但是这时候我们就会有疑惑了,使用sizeof的时候为什么显示的不是首元素的大小呢?
#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("%d\n", sizeof(arr)); return 0; }在这里面,输出的结果是40,和我们原先的猜想怎么不一样呢。但是其实这是属于极少数的个例,在C语言的编程中只有两种情况会发生不一样的结果,在其他情况下单独数组名的出现指向的都会是它首元素的地址。特殊情况如下所示
a. sizeof(数组名)在sizeof中存放数组名计算的是整个数组的大小而不是首元素的大小。
b. &(数组名)这里取出的是整个数组的地址,在实现数组加减的时候跨越的字节数量是不一样的,下面会用代码进行解释。
这两个例外只有在它们单独出现的时候才会生效,如果是处于叠加状态,比如说sizeof(arr+1)的情况下,那么arr数组名就仍然只会等于首元素的地址。为了更深入理解数组的性质,我们不妨再尝试一下这一串代码。
#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); printf("&arr = %p\n", &arr); return 0; }尝试后我们不难发现,三个打印结果一模一样,那么arr到底与&arr有什么区别呢?
//测试环境:X86 #include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("&arr[0]+1 = %p\n", &arr[0]+1); printf("arr = %p\n", arr); printf("arr+1 = %p\n", arr+1); printf("&arr = %p\n", &arr); printf("&arr+1 = %p\n", &arr+1); return 0; }输出结果
&arr[0] = 0077F820 &arr[0]+1 = 0077F824 arr = 0077F820 arr+1 = 0077F824 &arr = 0077F820 &arr+1 = 0077F848我们不难发现取出首元素地址与单个数组名的情况下都是仅仅向后跳过了4个字节,而取出&arr则是向后跳了整个数组也就是40个字节。证明了&arr确实是使用的取出整个数组的形式来存储与内存当中的,只是碍于表达形式只能打印出首元素的地址。
2.0使用指针访问数组
2.1arr[]实际上是*(arr+)的语法糖
我们不妨使用这么一个指针来访问数组
#include <stdio.h> int main() { int arr[10] = {0}; //输⼊ int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); //输⼊ int* p = arr; for(i = 0; i < sz; i++) { scanf("%d", p+i); //scanf("%d", arr+i);//也可以这样写 } //输出 for(i = 0; i < sz; i++) { printf("%d ", *(p+i)); } return 0; }通过这一个代码我们可以很清楚的发现通过指针不断相加的存储,然后再进行解引用就能输出其中的内容,对于数组而言,在没有进行[]处理的时候使用解引用对应指针的方式也能够打印出其中的数值。但我们此时肯定不禁会有一个疑惑为什么在使用scanf进行输入的时候,并不需要解引用也能存储改变数组空间当中的内容呢,这是因为在scanf的输入之时就会自动的解引用指向对应的空间,所以在使用scanf的时候我们都会在变量之前加上&符号,而对于数组名就直接相当于首元素地址的数组而言,就不需要了,这也是数组与普通变量之间的直接区别。
#include <stdio.h> int main() { int arr[10] = {0}; //输入 int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); //输入 int* p = arr; for(i = 0; i < sz; i++) { scanf("%d", p+i); //scanf("%d", arr+i);//也可以这样写 } //输出 for(i = 0; i < sz; i++) { printf("%d ", p[i]); } return 0; }我们运行这个代码,不难发现p[i]的结果与上面*(p+i)的结果是相同的,那么实际上这两个代码在运行的角度中就正是一样的,这样的情况正如标题所说称之为arr[]实际上是*(arr+)的语法糖,在指针的情况中如此,面对数组本体也能起到同样的效果
3.0一维数组传参的本质
3.1数组传参的实验
数组我们之前学过了,也讲过了。我们不妨来做一个实验来测试数组在函数当中是怎么传递的。到底是不是如同我们之前所说的那样传递的为首元素的大小。为了达成实验效果,我们将在函数中与函数外分别计算数组中的元素个数
#include <stdio.h> //测试环境是x86 void test(int arr[]) { int sz2 = sizeof(arr)/sizeof(arr[0]); printf("sz2 = %d\n", sz2); } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int sz1 = sizeof(arr)/sizeof(arr[0]); printf("sz1 = %d\n", sz1); test(arr); return 0; }得出的结果是意料之中的
在函数内部并没有正确获得数组中的元素个数。在上个小节中我们学到了数组的本质实际上是指针,而单独一个数组名存储的实际上是数组首元素的地址,而在此处函数的传参中,传递上去的实际上就不过是一个地址而已,并没有将整个数组完整的传递上去。于此同时sizeof计算的大小就是一个地址的大小而不是整体数组的大小,正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
void test(int arr[])//参数写成数组形式,本质上还是指针 { printf("%d\n", sizeof(arr)); } void test(int* arr)//参数写成指针形式 { printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩ } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; test(arr); return 0; }总结:一维数组传参无论是写成数组形式还是指针形式都行,两者在代码中代表的含义是相同的,这就是数组的本质。
4.0二级指针与指针数组
4.1二级数组的本质
变量有各种各样的类型,从总的方面来说,指针变量也是变量,那么既然是变量就会有储存的空间,也就产生了指向这个空间的地址,那么储存这一个地址的指针就被称之为二级数组。这个概念对于数据中的链表是十分的重要的,它代表了一个串联的线,往后可以由此组成一张大网,方便我们快速寻找到应有的数据。
它的基础代码是这样的
#include <stdio.h> int main() { int a = 10; int *pa = &a; int** ppa = &pa; return 0; }相比于一级指针来说,看起来好像不过只是多加了一个*,而在计算机的系统中,它却是这样表示
在实际上的意义中pa代表着一维指针指向的那个地点,而*pa则代表其指向的空间也就是a,那么*ppa实际上就是等于pa,而**ppa则代表着a,往后以此类推,哪怕是三维指针或者四维指针它们所代表的结果都是这样的了。
4.2指针数组
指针数组到底是数组还是指针呢?我相信大部分人在看见这个标题的时候都会产生这个疑惑。
但是正如同整形数组于字符数组是存放对应数据类型的数组,指针数组存放的就都是指针,这与后面我们会讲到的数组指针有很大的区别。
我们不妨先来看看这样一串代码,好让我们充分了解指针数组的初始化和创建方式
#include <stdio.h> int main() { int a = 10; int b = 20; int c = 30; int* arr[3] = {&a, &b, &c};//指针数组,存放的是三个int*类型的指针 int i = 0; for(i = 0; i < 3; i++) { printf("%d ", *(arr[i]));//通过指针解引用取出对应变量的值 } return 0; }对于指针数组来说必须要填充其中的具体数字,当初始化为0时,实际上是默认在里面创建了NULL指针,当你不规定具体的元素个数时,这里的指针数组如果有具体指向就会很容易导致程序崩溃,所以在使用的时候最好还是提前规定一下
4.3指针数组模拟二维数组
我们都知道二维数组来表示编号的时候是以X Y来为具体的元素空间填充序号的,那么在使用指针数组的过程中我们就可以模拟这种效果,指针数组里面的序号变为实际上二维数组中的列号,内部蕴含着的数组成为行号。代码实现如下
#include <stdio.h> int main() { int arr1[] = {1,2,3,4,5}; int arr2[] = {2,3,4,5,6}; int arr3[] = {3,4,5,6,7}; //数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中 int* parr[3] = {arr1, arr2, arr3}; int i = 0; int j = 0; for(i = 0; i < 3; i++) { for(j = 0; j < 5; j++) { printf("%d ", parr[i][j]); } printf("\n"); } return 0; }parr[i] 是访问 parr 数组的元素,parr[i] 找到的数组元素指向了整型一维数组,parr[i][j] 就是整型一维数组中的元素。
上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的。
从上面的代码和运行结果可以看出,指针数组模拟二维数组的核心思路是:用一个指针数组来存放多个一维数组的首元素地址,再通过双重下标访问的方式,把原本分散在内存中的多个一维数组,在逻辑上组织成一个类似二维数组的结构。其中 parr 是外层指针数组,parr[i] 取出第 i 个一维数组的首地址,parr[i][j] 则进一步访问该一维数组中的第 j 个元素。
这种写法在逻辑上非常直观,也便于理解指针与数组之间的紧密联系。但需要特别注意的是,它和真正的二维数组在内存布局上有着本质区别:真正的二维数组在内存中是连续存放的,所有元素按行紧密排列;而指针数组模拟出的"二维数组",每一行实际上是独立的一维数组,它们在内存中的地址并不连续,行与行之间可能存在空隙。因此,这种模拟方式更适合用于逻辑上的行列访问,如果对内存连续性有严格要求(例如需要整体拷贝或按连续内存块操作),仍然应当使用真正的二维数组。
总结:指针数组模拟二维数组,本质上是"用指针数组管理多个一维数组",它借助数组名即首元素地址的特性,把多个一维数组的首地址统一存放在一个指针数组中,再通过双重下标完成行列访问。这种方式代码简洁、逻辑清晰,是理解指针数组应用场景的经典示例;同时也要牢记它与真正二维数组在内存连续性上的差异,在实际开发中根据需求选择合适的数据组织方式。
