本文继续总结《C和指针》第8章的内容,本章的内容由3个部分构成:一维数组、多维数组、指针数组。
一维数组
通常定义一个整型变量使用 int var = 0;
如果想要定义一组整型变量,我们可以这样定义:
int a1 = 0;
int a2 = 1;
int a3 = 2;
int a4 = 3;
int a5 = 4;
// 这里才写了5个变量,我已经极不耐烦了,书写太麻烦
如果有一个"容器"可以把这些整型变量都"装"起来,作为一个整体。打个比方,假设一个整型变量就是一瓶矿泉水,5个整型变量就是5瓶矿泉水,在实际生活中,一件水是12瓶,如果没有箱子将12瓶水包装成一件,显然,搬运水的效率会很低。同样的道理,数组就相当于箱子,提高数据"搬运"的效率。
一维数组定义
语法形式:数据类型 数组名[数组长度];
int arr[10];
定义了一个整型数组,数组的长度为10,数组的每个元素是一个整型。
数组名是一个指针,一个指向类型的指针常量,常量的值是不能改变的。
我们一般将int a = 1;这样的变量叫做标量(可以联想数学上的定义),既然有标量,那么就会有向量,数组就相当于向量,存储的是相同数据类型的元素。
一维数组初始化
一个好的习惯时,在定义数组时进行初始化。例如:
int arr[10] = {0}; // 将数组的所有元素初始化为0
因为无论该数组作为全局变量还是局部变量,不依赖编译器进行初始化总是更加安全的。
一维数组访问
下标访问
一般来说,我们可以通过下标方式对数组的元素进行访问和修改。例如
arr[5] = 15; //表示将数组的第5个元素赋值为15
// 需要注意的是, c语言中数组的计数是从0开始的。
指针访问
int arr[10] = {1,2,3};
int *p_arr = &arr[0];
*(p_arr+5) = 15; //也表示将数组的第5个元素赋值为15
既然下标和指针都能访问和修改数组的元素,那么使用那种方式更好呢?这个没有确切的答案。如果从代码的阅读性和可维护性的角度来讲,下标访问的方式会更直观一些;如果从程序的运行效率的角度来讲,指针访问的方式的效率会更高一些。所以在工程中,需要根据实际情况去权衡。
一维数组参数
我们知道,标量可以作为函数的参数,那么数组是否也能用作函数参数?如果可以,要怎样使用呢?
#include <stdio.h>
#include <stdlib.h>
#define ARRAY_LEN(name, type) (sizeof(name)/sizeof(type)) // 获取数组的长度(元素个数)
// 函数原型声明
int func1(int arr[], int len); // 形参使用数组名
int func2(int *p_arr, int len); // 形参使用指针
// 函数定义
int func1(int arr[], int len)
{
int i = 0;
for (i = 0; i < len; ++i)
{
printf("数组的第%d个元素的值:%d\n", i,arr[i]);
}
return 0;
}
int func2(int *p_arr, int len)
{
int i = 0;
for (i = 0; i < len; ++i)
{
printf("数组的第%d个元素的值:%d\n", i, p_arr[i]);
}
return 0;
}
int main(int argc, char** argv)
{
int i = 0;
int a[10] = {0};
// 给数组赋值
for (i = 0; i < ARRAY_LEN(a, int); ++i)
{
a[i] = i+1;
}
func1(a, ARRAY_LEN(a, int));
func2(a, ARRAY_LEN(a, int));
return 0;
}
从这个例子中,我们可以发现
在函数原型声明时,一维数组形式的形参和指针形式的形参是等价的。本质上都是指针。因为数组名是一个指向某种数据类型的指针常量。
在函数调用时,传入的实参都是相同的,可以说明,在函数声明及使用的情形下,数组名和指针是等价的。(PS:仅仅是在这种使用场景下,可以这样表述。因为数组名和指针是两个不同的概念)
另外,我在定义函数时,第二个参数指定了数组的实际长度,如果不指定长度仅传递数组的地址的话,函数内部对数组的长度是未知的。
多维数组
多维数组定义
语法形式: 数据类型 数组名[][][]…
二维数组
int arr_2d[4][5];
三维数组
int arr_3d[3][4][5];
四维数组
int arr_4d[2][3][4][5];
多维数组初始化
#include <stdio.h>
#include <stdlib.h>
#define ARRAY_LEN(name, type) (sizeof(name)/sizeof(type))
int main(int argc, char** argv)
{
// 定义一个4行5列的二维数组
// 降维角度
// 可以将arr_2d看作是 一个一维数组,该数组有4个元素,每个元素是一个含5元素的一维数组
// 存储
// 二维数组的所有元素的存储是按照行主序来进行的,也就是说,将该数组的元素"展平"后,存储为1,2,3,4,5,11,12,13,14,15,21,22,23,24,25,31,32,33,34,35
// 行主序: 从数组下标的最右边的维度先存储;简单来说就是先行后列,从左到右,从上到下。
// 有些编程语言中,矩阵是列主序存储的,例如matlab.
int arr_2d[4][5] =
{ // 第一层括号
{1, 2, 3, 4, 5}, // 第二层括号
{11, 12, 13, 14, 15},
{21, 22, 23, 24, 25},
{31, 32, 33, 34, 35},
};
// 从花括号的层数也可以看出数组的维度,两层花括号,就是二维数组
// 在线性代数中,我们将其叫做矩阵(matrix)
// 定义一个三维数组
// 降维角度
// 可以将arr_3d看作是 一个一维数组,该数组有3个元素,每个元素是一个4行5列的二维数组
// 也可以将arr_3看作是 一个3行4列二维数组,该数组的每个元素是一个含5个元素的一维数组
int arr_3d[3][4][5] =
{ // 第一层括号
{ // 第二层括号
{0x01, 0x02, 0x03, 0x04, 0x05}, // 第三层括号
{0x11, 0x12, 0x13, 0x14, 0x15},
{0x21, 0x22, 0x23, 0x24, 0x25},
{0x31, 0x32, 0x33, 0x34, 0x35},
},
{
{0x101, 0x102, 0x103, 0x104, 0x105},
{0x111, 0x112, 0x113, 0x114, 0x115},
{0x121, 0x122, 0x123, 0x124, 0x125},
{0x131, 0x132, 0x133, 0x134, 0x135},
},
{
{0x201, 0x202, 0x203, 0x204, 0x205},
{0x211, 0x212, 0x213, 0x214, 0x215},
{0x221, 0x222, 0x223, 0x224, 0x225},
{0x231, 0x232, 0x233, 0x234, 0x235},
}
};
// 三层花括号就是三维数组
// 定义一个四维数组
int arr_4d[2][3][4][5] =
{ // 第一层括号
{ // 第二层括号
{ // 第三层括号
{0x0001, 0x0002, 0x0003, 0x0004, 0x0005}, // 第四层括号
{0x0011, 0x0012, 0x0013, 0x0014, 0x0015},
{0x0021, 0x0022, 0x0023, 0x0024, 0x0025},
{0x0031, 0x0032, 0x0033, 0x0034, 0x0035},
},
{
{0x0101, 0x0102, 0x0103, 0x0104, 0x0105},
{0x0111, 0x0112, 0x0113, 0x0114, 0x0115},
{0x0121, 0x0122, 0x0123, 0x0124, 0x0125},
{0x0131, 0x0132, 0x0133, 0x0134, 0x0135},
},
{
{0x0201, 0x0202, 0x0203, 0x0204, 0x0205},
{0x0211, 0x0212, 0x0213, 0x0214, 0x0215},
{0x0221, 0x0222, 0x0223, 0x0224, 0x0225},
{0x0231, 0x0232, 0x0233, 0x0234, 0x0235},
},
},
{
{
{0x1001, 0x1002, 0x1003, 0x1004, 0x1005},
{0x1011, 0x1012, 0x1013, 0x1014, 0x1015},
{0x1021, 0x1022, 0x1023, 0x1024, 0x1025},
{0x1031, 0x1032, 0x1033, 0x1034, 0x1035},
},
{
{0x1101, 0x1102, 0x1103, 0x1104, 0x1105},
{0x1111, 0x1112, 0x1113, 0x1114, 0x1115},
{0x1121, 0x1122, 0x1123, 0x1124, 0x1125},
{0x1131, 0x1132, 0x1133, 0x1134, 0x1135},
},
{
{0x1201, 0x1202, 0x1203, 0x1204, 0x1205},
{0x1211, 0x1212, 0x1213, 0x1214, 0x1215},
{0x1221, 0x1222, 0x1223, 0x1224, 0x1225},
{0x1231, 0x1232, 0x1233, 0x1234, 0x1235},
},
},
};
printf("2d len:%ld\n", ARRAY_LEN(arr_2d, int));
printf("3d len:%ld\n", ARRAY_LEN(arr_3d, int));
printf("4d len:%ld\n", ARRAY_LEN(arr_4d, int));
return 0;
}
多维数组访问
多维数组元素的访问和二维数组一样,也可以通过下标方式来访问和修改。
以四维数组为例,
int arr_4d[2][3][4][5] =
{ // 第一层括号
{ // 第二层括号
{ // 第三层括号
{0x0001, 0x0002, 0x0003, 0x0004, 0x0005}, // 第四层括号
{0x0011, 0x0012, 0x0013, 0x0014, 0x0015},
{0x0021, 0x0022, 0x0023, 0x0024, 0x0025},
{0x0031, 0x0032, 0x0033, 0x0034, 0x0035},
},
{
{0x0101, 0x0102, 0x0103, 0x0104, 0x0105},
{0x0111, 0x0112, 0x0113, 0x0114, 0x0115},
{0x0121, 0x0122, 0x0123, 0x0124, 0x0125},
{0x0131, 0x0132, 0x0133, 0x0134, 0x0135},
},
{
{0x0201, 0x0202, 0x0203, 0x0204, 0x0205},
{0x0211, 0x0212, 0x0213, 0x0214, 0x0215},
{0x0221, 0x0222, 0x0223, 0x0224, 0x0225},
{0x0231, 0x0232, 0x0233, 0x0234, 0x0235},
},
},
{
{
{0x1001, 0x1002, 0x1003, 0x1004, 0x1005},
{0x1011, 0x1012, 0x1013, 0x1014, 0x1015},
{0x1021, 0x1022, 0x1023, 0x1024, 0x1025},
{0x1031, 0x1032, 0x1033, 0x1034, 0x1035},
},
{
{0x1101, 0x1102, 0x1103, 0x1104, 0x1105},
{0x1111, 0x1112, 0x1113, 0x1114, 0x1115},
{0x1121, 0x1122, 0x1123, 0x1124, 0x1125},
{0x1131, 0x1132, 0x1133, 0x1134, 0x1135},
},
{
{0x1201, 0x1202, 0x1203, 0x1204, 0x1205},
{0x1211, 0x1212, 0x1213, 0x1214, 0x1215},
{0x1221, 0x1222, 0x1223, 0x1224, 0x1225},
{0x1231, 0x1232, 0x1233, 0x1234, 0x1235},
},
},
};
arr_4d数组的存储空间有120个int型的元素,这120个int在内存中是连续分配的,每个占4个字节。只不过我们在定义多维数组时,给它人为的加上一个逻辑,四维([2][3][4][5])。因为c语言中数组是行主序的,
printf("arr_4d[0][0][0][1]=0x%04x\n", arr_4d[0][0][0][1]); // arr_4d[0][0][0][1]=0x0002
printf("arr_4d[1][1][2][3]=0x%04x\n", arr_4d[1][1][2][3]); // arr_4d[1][1][2][3]=0x1124
多维数组的数组名比一维数组就相对复杂一点,但是也没啥可怕的
printf("%p\n", arr_4d); // int (*)[3][4][5]
printf("%p\n", arr_4d[0]); // int (*)[4][5]
printf("%p\n", arr_4d[0][0]); // int (*)[5]
printf("%p\n", arr_4d[0][0][0]); // int *
// 不同维度的数组名,代表的指针的含义是不同的。
// int * 是 指向int的指针,偏移单位是1个int
// int(*)[5] 是 指向一维数组的指针,该数组的长度为5,指针的偏移单位为5个int
// int(*)[4][5] 是 指向二维数组的指针,该数组的长度为20,指针的偏移单位为20个int
// int (*)[3][4][5] 是 指向三维数组的指针,该数组的长度为60, 指针的偏移单位为60个int
// 这四种形式的数组名的地址都是相同的
// 例如,(注意每次执行,地址的值都不想同,但4个值是相同的)
0x7ffc2f3b8790
0x7ffc2f3b8790
0x7ffc2f3b8790
0x7ffc2f3b8790
printf("%p\n", arr_4d); // 0x7ffe28d4c1b0
printf("%p\n", arr_4d[1]); // 0x7ffe28d4c2a0
//2a0-1b0=f0, 也就是十进制的240,因为每个int占4字节,所以刚好就是相差60个元素的地址。
//也就是说,在数组的第4维的角度来看,可以将arr_4d[2][3][4][5]看作是一个一维数组,里面有两个元素,每个元素是一个3*4*5的数组
// arr_4d[0][0][0] 相当于指向一维数组的指针,即int *
再看一个通过指针访问的例子
printf("arr_4d[1][1][2][3]=0x%04x\n", *(*(*(*(arr_4d+1)+1)+2)+3)); // 等价于arr_4d[1][1][2][3]
显然,对于高维数组,通过指针访问数组的元素,可读性很差。
再看一个另外一个指针访问的例子:
int *p = &arr_4d[0][0][0][0];
printf("arr_4d[1][1][2][3]=0x%04x\n", *(p+60+20+10+3)); // 等价于arr_4d[1][1][2][3]
// 这个例子是从数组元素存储的角度,来访问指定位置的元素。
// 因为第4维的偏移单位是60,所以 1*60 = 60
// 第3维的偏移单位是20, 所以 1*20 = 20
// 第2维的偏移单位是5, 所以 2*5 = 10
// 第1维的偏移单位是1, 所以 3*1 = 3
多维数组参数
前面我们看到了类似下面的指针形式:
printf("%p\n", arr_4d); // int (*)[3][4][5]
printf("%p\n", arr_4d[0]); // int (*)[4][5]
printf("%p\n", arr_4d[0][0]); // int (*)[5]
printf("%p\n", arr_4d[0][0][0]); // int *
这就是所谓的数组指针,即指向数组的指针。
在将一维数组作为形参的函数原型中,即可以将该参数写成数组的形式,也可以将该参数写成指针的形式。
但是对于多维数组,只有在第1维的时候可以既写成数组或写成指针,在高维时,必须以数组的形式来表明数组的长度信息。
假设,一个函数的形参是一个指向包含5个整型的数组的指针,那么函数的原型可以写成如下形式:
void func(int (*p)[5]);
void func(int arr[][5]); // 二者都可,我习惯用第一种形式,个人觉得更顺眼!
指针数组
前面介绍的数组的元素都是int类型,一般数组的元素可以是基本数据类型,但若是要表达更复杂的数据类型,有时我们虽然可以通过可见的结构体,来构造结构体数组表达复杂的数据类型,但这似乎还是不够灵活,特别是对于不可见的数据结构时。使用指针来表示不可见的数据结构,似乎简单可行。
指针数组定义
指针数组就是 数组的元素 都是 指向某种类型的指针。
简单的文件描述符hash就可通过指针数组来实现,数组的索引代表文件描述符fd,数组的元素(指针)代表一个队列,队列中的节点存储相应的数据。
指针数组初始化
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char** argv)
{
int i = 0;
int *p_arr[10] = {NULL};
for (i = 0; i < 10; ++i)
{
printf("p[%d]=%p\n", i, p_arr[i]);
}
return 0;
}
关注我
我的公众号二维码,欢迎关注
QQ讨论群:679603305