二维数组
一个二维数组,在本质上,是一个一维数组的列表。声明一个 x 行 y 列的二维整型数组,形式如下:
type arrayName [x][y];
这个表示,有x个一维数组,每个一维数组的元素个数是y个。
声明示例:
/** 定义数组 */ int main() { int ar[3][4]; // 3 行 4列 未初始化 char br[3][4]; double cr[3][4]; return 0; }
一个二维数组,在本质上是有多个一维数组构成。(每一个一维数的大小必须相同)
例如:定义 int ar[3][4] 的二维数组,它是由3个一维数组组成,每个一维数组的大小是4个整型元素。可以只对部分元素赋值,未赋值的元素自动取 0 值。
可以进行如下赋值:
int x[3][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}};
如果对二维数组的初始化,那么第一维的长度是可以缺省的,但是第二维不可缺省。
int main() { int ar[][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 }; // 3 行 4 列 int br[][4] = { {1,2},{3,4},{5,6} }; // 3 行 4 列 数字不足自动补 0 int cr[][4] = { 1,2,3,4,5,6,7,8 }; // 2 行 4 列 return 0; }
注:里面的花括号是可以省略的,如果省略,那么按照列的个数来自动分配。
验证如下:
#include <stdio.h> int main() { int a[][3] = {1, 2, 3, 4, 5}; /* 我的第一个 C 程序 */ printf("Hello, World! %d\n", a[0][2]);//Hello, World! 3 printf("Hello, World! %d\n", a[1][2]);//Hello, World! 0 return 0; }
二维数组在内存中的存储
二维数组的逻辑表示:
二维数组的物理表示(按行优先存储):
二维数组的数组名有什么含义
先来梳理一下一维数组的数组名含义。
#include<stdio.h> int main(void){ int a[3]={1,2,3}; int *p; p=a; printf(" a=%d\n",a); printf(" &a[0]=%d\n",&a[0]); printf(" &a=%d\n",&a); printf(" *a=%d\n",*a); printf(" *p=%d\n",*p); }
我们知道数组是在内存里占据一片连续的内存空间,由此可以看到,数组名a的值为数组a的第一个元素的地址,且数组名a自身的地址也和a指向的地址相同,即 a=&a=&a[0],但是要注意,&a只是在数值上相同,在含义上并不相同。
对于一维数组而言,&a,即数组本身的指针,并没有什么用处,重要的是a,即数组首元素的指针。由main函数中开始的两行代码,我们能知道,如果想要接收数组的数组名,我们需要定义的指针类型是数组元素的类型,而不是定义一个数组指针。当然,如果想要接收&a,那么就需要定义数组指针。
先根据一维数组的函数名含义来猜想下二维数组的函数名含义。
如a[3][4],那么,a就代表着第一个一维数组的地址。也就是说,是个数组的地址。
另外,a[0]、a[1]、a[2]都表示一个一维数组,是否也表示各一维数组的地址。
测试:编辑如下代码:
#include <stdio.h> int main() { int a[3][4] = {{1, 2 , 3, 4}, {5, 6 , 7, 8}, {9, 10 , 11, 12}}; printf("Hello, World! %x\n", a); printf("Hello, World! %x\n", a[0]); printf("Hello, World! %x\n", a[1]); printf("Hello, World! %x\n", a[2]); return 0; }
如果上述猜想是对的,那么第一行和第二行的数值是一样的,且后面的三个地址,依次相差16个字节。
运行:
符合猜想。
为了进一步证实,用他们访问具体的数据。
#include <stdio.h> int main() { int a[3][4] = {{1, 2 , 3, 4}, {5, 6 , 7, 8}, {9, 10 , 11, 12}}; printf("Hello, World! %d\n", *(int *)(a)); printf("Hello, World! %d\n", *((int *)a[0] + 1)); printf("Hello, World! %d\n", *(int *)a[1]); printf("Hello, World! %d\n", *((int *)a[2] + 3)); return 0; }
如果没错的话,应该依次输出1、2、5、12
运行:
以为正确。
后来,又遇到一个问题,才发现上面的结论并不完全对。
先说进一步发现的结论:
对于二维数组来说,数组名a表示首元素的地址;a[0]相当于一维数组的数组名,表示的是第一个一维数组的首元素的地址。
可以这么理解,对二维数组名进行解引用,能得到一维数组名的效果。
#include <stdio.h> int main() { int a[][3] = {{1, 2, 3},{4, 5, 6}}; printf("result is %d\n", **a); printf("result is %d\n", *a[0]); printf("result is %d\n", *a[1]); return 0; }
运行结果
二维字符数组
常规的字符数组按照上述的内容来考虑。
我们这里重点关注字符串形式的二维数组。char a[][10] = {"be", "happy", "everyday"};
考虑上述代码,其中a是个双重指针,指向首元素;a[0]是“be”字符串,指向'b';a[1]是“happy”字符串,指向'h';a[2]是“everyday”字符串,指向'e'。
#include <stdio.h> int main() { char a[][10] = {"be", "happy", "everyday"}; printf("result is %s\n", *a); printf("result is %s\n", a[0]); printf("result is %s\n", a[1]); printf("result is %s\n", a[2]); printf("result is %c\n", **a); printf("result is %c\n", *a[0]); printf("result is %c\n", *a[1]); printf("result is %c\n", *a[2]); return 0; }
运行结果如下:
注意,直接%s打印字符指针,可以直接输出字符串
#include <stdio.h> int main() { char *p = "everyday"; printf("result is %s\n", p); return 0; }
运行结果:
从哪里开始打印,就打印其之后的部分字符串
#include <stdio.h> int main() { char *p = "everyday"; printf("result is %s\n", p + 5); return 0; }
运行结果:
补充说明:
二维字符数组和字符指针数组挺像的。
char a[][10] = {"be", "happy", "everyday"}; char *a[] = {"be", "happy", "everyday"};
但是要注意,字符指针数组本质上是个一维数组。
不过,这两者在a、a[0]、a[0]、a[0]上是等效的,比如将上面的一个例子中的二维数组改成一维指针数组,运行结果不变。
#include <stdio.h> int main() { char *a[] = {"be", "happy", "everyday"}; printf("result is %s\n", *a); printf("result is %s\n", a[0]); printf("result is %s\n", a[1]); printf("result is %s\n", a[2]); printf("result is %c\n", **a); printf("result is %c\n", *a[0]); printf("result is %c\n", *a[1]); printf("result is %c\n", *a[2]); return 0; }
运行结果如下:
二维数组作为函数形参
如果想要把二维数组作为函数形参,可以直接用二重指针来接收。
二重指针
二重指针与普通一重指针的区别
本质上来说,二重指针和一重指针的本质都是指针变量,指针变量的本质就是变量。
一重指针变量和二重指针变量本身都占4字节内存空间,二重指针的本质
二重指针本质上也是指针变量,和普通指针的差别就是它指向的变量类型必须是个一重指针。二重指针其实也是一种数据类型,编译器在编译时会根据二重指针的数据类型来做静态类型检查,一旦发现运算时数据类型不匹配编译器就会报错。
C语言中如果没有二重指针行不行?其实是可以的。一重指针完全可以做二重指针做的事情,之所以要发明二重指针(函数指针、数组指针),就是为了让编译器了解这个指针被定义时定义它的程序员希望这个指针被用来指向什么东西(定义指针时用数据类型来标记,譬如int *p,就表示p要指向int型数据),编译器知道指针类型之后可以帮我们做静态类型检查。编译器的这种静态类型检查可以辅助程序员发现一些隐含性的编程错误,这是C语言给程序员提供的一种编译时的查错机制。
为什么C语言需要发明二重指针?原因和发明函数指针、数组指针、结构体指针等一样的。指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
一个指向指针的指针变量必须如下声明,即在变量名前放置两个星号。例如,下面声明了一个指向 int 类型指针的指针:
int **var;
当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,如下面实例所示:
二重指针的用法
二重指针指向一重指针的地址
二重指针指向指针数组实践编程中二重指针用的比较少,大部分时候就是和指针数组纠结起来用的。
实践编程中有时在函数传参时为了通过函数内部改变外部的一个指针变量,会传这个指针变量的地址(也就是二重指针)进去比如:
void find_max_and_min(int **pmax,int **pmin, int arr[]) { *pmax = *pmin = arr; int i; for(i=0;i<10;i++) { if(**pmax < arr[i]) { *pmax = arr+i; } if(**pmin > arr[i]) { *pmin = arr+i; } } }