写在前面
数组是一种简单基本的数据结构,C语言对这种数据结构的描述很少,对它的限制也非常少,这导致它实在不够抽象,基本功能非常受限。(总感觉这是C对底层性的妥协,如果真的搞出来一个类似python中的元组,确实有些违和感)
这让我感觉到,C语言的数组似乎只是用指针搞出来的宏定义。
但同时,这种很少的限制也为程序员带来了极大的自由发挥的空间。
下面我将介绍三种不同的方法来使用多维数组。
这里我并没有分类,因为它们在分类上存在交叉。
例如:
从数据类型的区别来看,1 和 2 为同类型,3 为另一类型。
从维度长度的特点上来看,1 和 3 为同类型,2 为另一类型。
(本篇仅以二维数组为例,多维数组的思想与之相同)
1.普通 方法(静态数组)
这种方法我们应该都不陌生,这是最普通的方法,也是我们第一次接触数组时所使用的方法。
特点:
- 固定长度(每一个维度的长度均相同)
- 在内存中连续(即线性排布)
声明并初始化:
int arr[2][3] = { {1,2,3} ,{4,5} };
在内存中:
这种方法会让数组的功能受限,
首先,我们需要在定义时就考虑清楚数组的尺寸,为避免数组越界,要让它足够大,于是多余的空间将被浪费。
其次,静态分配的数组会被存放在栈空间,而栈空间的大小很有限,如果我们需要一个尺寸很大的数组,那么你的程序可能刚跑起来就内存溢出了。
“分配爆栈一气呵成,是编译器中的豪杰”。
2.指针的指针 方法
抽象原理
我很喜欢这种方法,它非常自由。
在叙述原理之前,首先我们要搞明白,在C语言中,只有两大块指针类型
第一种叫 “指针”,第二种叫 “指针的指针”。
这时候你可能会问了,那为什么没有 “指针的指针的指针” 或者 “指针的指针的指针的指针” 呢?这要从这两种指针所指的对象来解释。
“指针” 指向一个除指针外的数据类型,例如 int* 指向一个 int 型内存空间。
“指针的指针” 则指向的是一个指针类型,那么对于 “指针的指针的指针” 不论我们再添多少个指针,他始终是指向一个指针的,也就同样是指针的指针。
这里配合我的这篇文章 【一些感悟】内存和指针 的指针对变量的引用原理小节食用更佳。我认为我们有必要搞清楚不同指针之间的本质区别到底在哪里。
于是我们就可以定义一个指针的指针数组,那么每一个元素都是一个指向指针的指针,这些指针可以指向任意一个数组的首地址。
抽象图:
特点:
- 任意维度的长度可以不同
- 在内存中可以不连续
这种引用方法非常地自由,他可以随意操作内存中不必连续的几段数据。
由于我们只是定义了一组指针的指针,所以我们每一个元素指向的数组指针也就不必连续。
而后,我们也不会关心我们指向的一系列数组到底是什么,那么这些数组的尺寸和维度也就可以任意定义,不必相同。
静态实现
一般来说,我们会通过动态分配内存的方式来使用这种方法,因为动态分配更加灵活。但当然,它也允许我们使用静态分配内存的方法。
那么我们先从我们熟悉的静态数组开始:
//第二维度
int* arr_1[4] = { 1,2,3,4 }, arr_2[4] = { 0 }, arr_3[4] = { 5,6,7 }, arr_4[4] = { 8 };
//第一维度:指针的指针
int** arr[4] = { arr_1,arr_2,arr_3,arr_4 };
引用方法:
for (size_t i = 0; i < 4; i++)
{
for (size_t j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}printf("
");
}
输出:
1 2 3 4
0 0 0 0
5 6 7 0
8 0 0 0
非常符合预期。
动态实现
这是更加灵活的使用方式。
既然我们已经明白了它的原理,也看了静态分配的代码,那么这里就不多说废话了,直接上代码:
//声明一个 int* 的指针,并分配 NUM1 个 int 长度的空间
int** arr = (char**)malloc(NUM1 * sizeof(int*));
//令每个元素指向一个指针,此指针其后的内存空间被分配了 NUM2 个 int 长度的空间
for (size_t i = 0; i < NUM1; i++)
{
arr[i] = (char*)malloc(NUM2 * sizeof(int));
}
引用方法同静态数组。
关于动态分配内存的一个技巧,移步我的这篇文章 → 【技巧】动态分配内存的一个简便用法
3.行指针 方法
实际上,我认为 行指针 只是为了便于初学者理解而起的一个名字,他可以与数组无关,或者说与所谓的 抽象行 无关,他是一种特殊的数据类型,这个作为拓展稍后再讲,我们先简单介绍一下行指针的基本用法。
实现和误区
与第二种方法类似,行指针也是指针的指针,他的基本原理也是指向任意一个数组的首地址。然而不同的是,当我们对行指针进行算数运算时,他会直接跳往我们所声明的下一行的首元素。
先看一下声明吧:
int* (*arr)[NUM] = NULL;
需要我们注意(也是理解行指针的关键)的是,中括号中的常量表达式并非意味者要为这个数组分配一个尺寸为常量值大小的内存空间,这与我们前面提到的数组声明的含义完全不同。
也就是说,操作系统仅仅开辟了一个指针大小的空间来存放一个行指针。
那么,中括号中的值又代表什么含义呢?
注意到我之前说的这句话:
然而不同的是,当我们对行指针进行算数运算时,他会直接跳往我们所声明的下一行的首元素。
也许你猜到了,是的,这里的常量值代表这个行指针所指向的数组中一行的元素个数,它告诉我们的行指针,“你一行有 NUM 个数据”。
特点:
- 固定长度(每一个维度的长度均相同)
- 在内存中连续(即线性排布)
引用方法:
int* arr_1[16] = { 1,2,3,4,5,6,7,8 };
int* (*arr)[4] = arr_1;
for (size_t i = 0; i < 4; i++)
{
for (size_t j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}printf("
");
}
或者这样写:
int* arr_1[16] = { 1,2,3,4,5,6,7,8 };
int* (*arr)[4] = arr_1;
for (size_t i = 0; i < 4; i++)
{
for (size_t j = 0; j < 4; j++)
{
printf("%d ", (*(arr + i))[j]);
}printf("
");
}
由于一个二维数组在内存中也是线性的,故这样写也可以:
int* arr_1[4][4] = { {1,2,3,4},{5,6,7,8} };
int* (*arr)[4] = arr_1;
for (size_t i = 0; i < 4; i++)
{
for (size_t j = 0; j < 4; j++)
{
printf("%d ", (*(arr + i))[j]);
}printf("
");
}
输出均为:
1 2 3 4
5 6 7 8
0 0 0 0
0 0 0 0
它对于处理一段连续的数据非常有效,它与一个指向数组首元素的指针的区别在于,行指针知道自己指向的是二维数组,而指针认为自己指向的是一个一维数组。
带有私货的拓展
夹带私货,可以参考,不要轻信
在本小节开始时,我说 行指针是一种特殊的数据类型,为什么?特殊在哪里?
在第二个方法中我提到了我的这篇文章:【一些感悟】内存和指针 的指针对变量的引用原理小节,如果你没有读的话,我建议你先简单看一看。
我们现在知道了,不同指针类型的本质区别就是其所指地址往后所能引用的空间大小,表现在算数预算上即为 该指针 + 1 后向后移动的字节数。
这时候我们发现了,我们虽然无法令行指针引用我们定义的其后的空间,但我们可以在声明时定义其 + 1 后其向后移动的字节数。
也就是说,我们可以通过行指针定义任意尺寸的一个数据结构,与结构体有些相似。
这是我目前的认识,具体应用还没有任何想法。
就当留个坑吧。
结束!