指针 数组 指针数组和数组指针

指针, 数组, 指针数组, 和数组指针

这里, 更多的并不是教给大家什么是数组, 什么是指针数组. 更多的, 是教给大家理解数组和指针的本质.

指针

详见/2019/11/09/zhi-zhen-shi-sha/

数组

一维数组

定义:
1
元素类型 数字名称[数组大小];
使用:

对于定义如下的一个数组:

1
int data[10] = {0};
使用元素:
1
data[i] = 1;
使用名字:

data 是 数组"首元素"的指针

1
2
printf("%p", data);
printf("%p", &data[0]);

注意, data是"右值", 不可赋值.

不知道右值是啥的快去看上一篇推送

地址运算:
+ 1```在数值上等于`data`的值+4. (int占4个字节)
1
2
3
4
5
6
7
8
9
10

复习: 指针+1, 实际指向的地址加了`sizeof(指向的类型)`.

#### 二维数组

二维数组就是"数组的数组". 如何理解这一点呢?

##### 二维数组的定义:
```cpp
int data[5][10];

上述代码定义了一个数组, 这个数组的元素是"10个int类型构成的数组". 如何理解? 数组定义的阅读要"从里向外阅读". 例如:

int data[5][10];

定义一个数组, 名字叫data, 大小为5

int data[5][10];

数组的元素的类型是int [10], 也就是大小为10的int数组.

数据类型: 如果不好理解 int [10] 是一个类型, 只需要记住 去掉定义变量时候的变量名字, 剩下的东西就是一个类型. 如:

对于 int data[10], 去掉data后剩下的部分int [10] 就是变量类型.

同理, 下面要讲到的 int (*)[10] 也是变量类型

二维数组元素的使用和一位数组基本相同, 这里不再赘述.

二维数组的地址的使用:

小问答: 对于定义如下的数组:

1
int data[10][20] = {0};

data+1的地址数值上等于多少?

1
2
3
A. data + 4(数值上)
B. data + 40(数值上)
C. data + 80(数值上)
1
左滑查看答案 ----------------------------------------> 由于data是"元素为int [20]"的数组, 所以 sizeof(data的元素) 是 20*4 = 80, 所以 data+1 的地址数值上等于 data + 80.

复习: 对于如上定义, data 的元素的类型为"int [20]". 实际上, 如果通过下标计算地址, 应该如下计算:

1
data[i][j] == *(data + i) + j;

如何理解上面的代码? *(data + i) 的类型又是什么?

1
2
左滑查看答案 ----------------------------------------> data的元素为 int[20], 因此 *(data + i) 是 data数组的第i个元素, 也就是第i个大小为20int数组.
左滑查看答案 ----------------------------------------> data[i][j] 是data中的第i个元素的第j个元素. (data的第i个元素是一个一维数组.)

(复习: *运算符是"根据地址取所在那个地址的元素")

通过上面的练习, 相信大家也理解了"二维数组就是元素是数组的数组".

这样子, 也就很容易能理解为什么二维数组作为参数要传入"第二维"的大小了. 对于以下代码:

1
2
3
4
5
6
7
8
int func1(int data[]){
这个函数的参数data是一个数组, 元素是int
并不需要知道数组的大小
}
int func2(int data[][20]){
这个函数的参数data是一个数组, 元素是 int[20]
因为需要知道元素的类型, 因此必须知道数组第二维的大小.
}

多维数组

多维数组的理解和二维数组相同, 比如三维数组是"元素为二维数组的数组". 因为并不常用, 这里不再赘述.

数组作为函数参数

首先, 需要说明的一点是, 数组作为函数参数的时候, 对于被调用的函数, 参数是一个指针. 无论传入什么数组, 对于被调用者, 参数的类型都是一个指针. 因此, 以下几个函数原型是等价的:

1
2
3
int foo(int *a, int n);
int foo(int a[], int n);
int foo(int a[10], int n);

而同时, 对于被调用的函数, 参数永远是一个指针, 因此对于上面的第三个函数原型, 传入一个类型为int[20]的数组也是可以编译通过的.

也正因为这个原因, 被调用的函数没有办法知道传入的数组的大小, 也没有办法限制传入的数组的大小, 因此只能显式的传入另一个参数n作为数组大小.

同理, 参数是一个指针, 需要知道指针指向的类型是什么. 这一点也同样可以理解为什么二位数组传参的时候要指定第二维的大小:

1
2
int func2(int data[][20]);
int func2(int (*)data[20]);

这两种声明等价. 第二种声明的参数类型是"指向一个int[20]"的指针(也就是下面会讲到的数组指针), 显然, 需要知道指向的数组多大, 因此必须说明第二维的大小.

指针数组

指针数组, 从字面意思来看, 就是元素是指针的数组.

定义:

1
int *data[10] = {NULL};

注意, 一定要初始化, 否则会有问题.

使用:

1
2
3
4
5
for(int i = 0; i < 10; ++ i){
data[i] = (int*)malloc(sizeof(int) * 10);
还记得为啥要(int*)吗?
}
data[1][2] = 1;

阅读:

对于上面的定义和初始化, data是指针数组的名字, 也就是指向指针数组首元素的指针. (指针的指针). data[i]data这一个数组的第i个元素, 也就是一个指向int的指针. 指针可以当成数组来使用, data[i][j]*(data[i] + j)是等价的.

经过上述代码创建的一个指针数组data的使用和int data[10][10]基本相同, 区别在于后者保证数组和数组之间的内存地址是连续的. data[0][9]data[1][0] 是连续的, 而如果使用指针数组方式创建的data, 不能保证 data[0][9]data[1][0] 在内存上连续.

数组指针

数组指针, 从字面意思来看就是"指向数组的指针".

定义

1
int data(*)[10] = NULL;

上述代码定义了一个指向长度为10的int数组(int [10])的指针.

使用

一般, 我们并不会使用到数组指针. 当且仅当一个情况下我们会在我们不知不觉的时候使用数组指针:

1
2
int func(int data[][20]){
}

前面提到过, 数组作为参数传入函数的时候, 对于被调用的函数参数就是指针. 因此, 这里参数是一个"元素为int[20]"的数组(数组的数组), 因此, 在函数内部, data实际上就是一个"指向int[20]"的指针(int (*)[20]).

一般并不需要使用数组指针的性质, 当编译器报错有int (*)[20]相关的东西时, 知道这是一个指向数组的指针即可.


注意

数组指针和指针数组的定义特别相似.

1
2
int *data[10] = {NULL}; 
int (*)data1[10] = NULL;

上述代码中, data是指针数组(指针的数组), data1是数组指针(数组的指针)

不要对数组和指针使用sizeof.

上面提到过, 对于被调用的函数, 数组参数实际上就是一个指针, 在函数内部对数组进行sizeof获取到的其实是指针占用的内存的大小(4字节或8字节). 同时, 对数组和指针使用sizeof会导致各种神奇的行为, 因此尽量不要对数组和指针使用sizeof. 当且仅当如malloc(10 * sizeof(int))时使用sizeof.


小测验

1
int data[10];

上述代码中, data的类型是?

1
左滑查看答案 ----------------------------------------> int*

1
2
int (*)[5]
int *[5]

上述两个类型中, 哪个是数组指针, 哪个是指针数组?

(复习: 定义变量的语句去掉变量名字剩下的部分就是变量类型. 如int *data[5]去掉data后剩下的int *[5] 是一个类型.)

1
左滑查看答案 ---------------------------------------->  前者是数组指针, 后者是指针数组

1
int data[5][5];

上述代码中, data的类型是?

1
左滑查看答案 ----------------------------------------> int (*)[5], 也就是指向 int[5] 的指针

1
int data[10][20];

data+1的值 和 data的值 在数值上差多少?

1
左滑查看答案 ----------------------------------------> 差一个 sizeof(int [20]), 也就是80字节.

鸣谢:

感谢云朵学长对本人不厌其烦的教导.

感谢优秀的Z同学在我找C语言文档时慷慨提供的C Primer Plus.

感谢我的舍友以及另一位优秀的Z同学的审稿.

(你居然看到了这里, 那就说一下吧). 有一个很常见的, 数组名字作为参数同时不传入数组大小的使用情景, 同学们知道是什么吗?

字符串相关函数. gets, puts, strlen, scanf等函数均只接受字符串名字作为参数, 他们是怎么获取字符串长度信息的呢?

明天再发