创建指向二维数组的指针

137

我需要一个指向静态二维数组的指针。如何实现这一点?

static uint8_t l_matrix[10][20];

void test(){
   uint8_t **matrix_ptr = l_matrix; //wrong idea 
}

我遇到各种错误,例如:

  • 警告:指针类型不兼容的赋值
  • 下标值既不是数组也不是指针
  • 错误:无效使用可变大小数组成员

1
@JohannesSchaub-litb 那已经不存在了。(我怎么再次查看它呢?我知道低声望成员可以查看它,但我忘记了怎么做...) - Mateen Ulhaq
1
@muntoo:这是它的副本:https://gist.github.com/sharth/ede13c0502d5dd8d45bd - Bill Lynch
10个回答

160

在这里,您想要创建一个指向数组第一个元素的指针。

uint8_t (*matrix_ptr)[20] = l_matrix;

使用typedef,这看起来更加简洁

typedef uint8_t array_of_20_uint8_t[20];
array_of_20_uint8_t *matrix_ptr = l_matrix;

然后你就可以重新享受生活啦 :)

matrix_ptr[0][1] = ...;

在C语言中要小心指针/数组的世界,这里充满了许多混淆。


编辑

浏览了一些其他答案,因为评论区太短,无法在那里进行。提出了多种替代方案,但并没有展示它们的行为方式。以下是它们的表现。

uint8_t (*matrix_ptr)[][20] = l_matrix;

如果您修复该错误并像下面的片段一样添加取地址运算符&

uint8_t (*matrix_ptr)[][20] = &l_matrix;

然后创建一个指向类型为20个uint8_t数组类型的元素的不完整数组类型的指针。由于指针是指向数组的数组,因此必须使用以下方式访问它:

(*matrix_ptr)[0][1] = ...;

因为它是指向不完整数组的指针,所以你不能使用快捷方式

matrix_ptr[0][0][1] = ...;

由于索引需要知道元素类型的大小(索引意味着将整数添加到指针,因此它无法使用不完整的类型)。请注意,这仅适用于C语言,因为T[]和T[N]是兼容的类型。C++没有“兼容类型”的概念,因此它会拒绝该代码,因为T[]和T[10]是不同的类型。
以下替代方案完全不起作用,因为当您将其视为一维数组时,数组的元素类型不是uint8_t,而是uint8_t[20]。
uint8_t *matrix_ptr = l_matrix; // fail

以下是一个很好的替代方案。
uint8_t (*matrix_ptr)[10][20] = &l_matrix;

您可以通过以下方式访问它:

(*matrix_ptr)[0][1] = ...;
matrix_ptr[0][0][1] = ...; // also possible now

它的好处在于保留了外部尺寸的大小。因此,您可以对其应用 sizeof。

sizeof (*matrix_ptr) == sizeof(uint8_t) * 10 * 20

还有一种答案利用了数组中元素是连续存储的事实。

uint8_t *matrix_ptr = l_matrix[0];

现在,这仅允许您访问二维数组的第一个元素的元素。也就是说,以下条件成立。
matrix_ptr[0] = ...; // valid
matrix_ptr[19] = ...; // valid

matrix_ptr[20] = ...; // undefined behavior
matrix_ptr[10*20-1] = ...; // undefined behavior

你会注意到它可能在10*20-1之前工作,但是如果你添加别名分析和其他激进的优化,一些编译器可能会做出破坏该代码的假设。话虽如此,我从未遇到过编译器在这上面失败的情况(但是我从未在真实代码中使用过这种技术),即使C FAQ也包含了这种技术(并警告其UB性质),如果你无法更改数组类型,则这是拯救你的最后一种选择 :)

+1 - 对于 int (*)[][20] 的分解,提供了不错的信息 - 在 C++ 中无法实现。 - Faisal Vali
@litb,很抱歉,您的解决方案是错误的,因为它没有为数组分配任何存储空间。 - Rob Wells
3
@Rob,我不太理解你的意思。在所有这些情况下,存储都是由数组l_matrix本身提供的。指向它们的指针从声明它们的位置(堆栈、静态数据段等)获取存储空间。 - Johannes Schaub - litb
1
@Sohaib - 不,那只会创建一个指针。你可能把它和uint8_t *d[20]混淆了,后者创建了一个指向uint8_t的3个指针的数组,但在这种情况下不起作用。 - Palo
你的意思是C标准不允许用一维指针访问多维数组吗? 这对于我来说是完全的新闻,而且与其他问题的答案相矛盾,比如这个例子: https://dev59.com/LVoV5IYBdhLWcg3wIb74 - cesss
显示剩余3条评论

39

为了充分理解这个问题,你必须掌握以下概念:

数组不是指针!

首先(已经有足够的人宣扬过),数组不是指针。相反,在大多数用法中,它们会“退化”为其第一个元素的地址,可以赋给指针:

int a[] = {1, 2, 3};

int *p = a; // p now points to a[0]

我认为它的工作方式是这样的,以便可以访问数组的内容而不必复制所有内容。这只是数组类型的一种行为,不意味着它们是同一件事。



多维数组

多维数组只是一种以编译器/计算机可以理解和操作的方式“分割”内存的方法。

例如,int a [4] [3] [5] = 一个包含4 * 3 * 5(60)个整数大小内存块的数组。

与使用普通的int b [60]相比,int a [4] [3] [5]的优点在于它们现在被“分割”了(如果需要,更容易处理它们的“块”),并且程序现在可以执行边界检查。

实际上,int a [4] [3] [5]在内存中的存储方式与int b [60]完全相同-唯一的区别是程序现在管理它们,好像它们是某些大小的单独实体(具体来说,四组三组五)。

请记住:无论是int a [4] [3] [5] 还是int b [60]都是存储在内存中的相同数据,唯一的区别是应用程序/编译器如何处理它们。

{
  {1, 2, 3, 4, 5}
  {6, 7, 8, 9, 10}
  {11, 12, 13, 14, 15}
}
{
  {16, 17, 18, 19, 20}
  {21, 22, 23, 24, 25}
  {26, 27, 28, 29, 30}
}
{
  {31, 32, 33, 34, 35}
  {36, 37, 38, 39, 40}
  {41, 42, 43, 44, 45}
}
{
  {46, 47, 48, 49, 50}
  {51, 52, 53, 54, 55}
  {56, 57, 58, 59, 60}
}

从这里可以清楚地看到,每个“分区”只是程序跟踪的一个数组。



语法

现在,数组在语法上与指针不同。具体来说,这意味着编译器/机器会对它们进行不同的处理。 这可能看起来很明显,但看看这个例子:

int a[3][3];

printf("%p %p", a, a[0]);

上面的示例会打印相同的内存地址,如下所示:

0x7eb5a3b4 0x7eb5a3b4

然而,只能将其中一个分配给指针,因此直接:

int *p1 = a[0]; // RIGHT !

int *p2 = a; // WRONG !

为什么不能将a赋给指针,但可以将a[0]赋给指针?

这是多维数组的一个结果,我将解释其原因:

在'a'的层次上,我们仍然可以看到另一个要考虑的“维度”。而在'a [0]'的层次上,我们已经处于最高维度,因此就程序而言,我们只是在查看普通数组。

您可能会问:

为什么多维数组在制作指向它的指针时很重要?

最好这样思考:

从多维数组的“衰变”不仅是一个地址,而是带有分区数据的地址(也就是说,它仍然知道其底层数据由其他数组组成),这些数据由第一维度之外的数组设置的边界组成。

除非我们指定,否则这种'分区'逻辑无法存在于指针中:

int a[4][5][95][8];

int (*p)[5][95][8];

p = a; // p = *a[0] // p = a+0
否则,数组的排序属性的意义将会丢失。
还要注意在 *p 周围使用括号:int (*p)[5][95][8] - 这是为了指定我们正在创建一个具有这些边界的指针,而不是具有这些边界的指针数组:int *p[5][95][8]

结论

让我们回顾一下:

  • 如果数组在使用的上下文中没有其他目的,它们会退化为地址
  • 多维数组只是数组的数组 - 因此,“退化”的地址将承载“我有子维度”的负担
  • 维数数据不能存在于指针中 除非你给它

简言之:多维数组退化为带有理解其内容能力的地址。


2
答案的第一部分很好,但第二部分不正确。这个是错误的:int *p1 = &(a[0]); // RIGHT !,实际上它与 int *p1 = a; 是相同的。 - 2501
@2501 感谢您发现错误,我已经更正。我不能确定为什么定义这个“规则”的示例也违反了它。值得重申的是,仅仅因为两个实体可以被解释为指针并产生相同的值,并不意味着它们具有相同的含义。 - Super Cat
只有编译器会将数组和指针区分对待。在运行时,数组退化为仅仅是常量指针。 - John Strood
首先(这已经被反复强调了),数组不是指针。抱歉,虽然您的回答很好,但这种傲慢完全是不必要的。 - Markus

7

In

int *ptr= l_matrix[0];

您可以这样访问:

*p
*(p+1)
*(p+2)

毕竟,2维数组也是存储为1维的。


*(p+k) 就是 p[k] 的简写! - John Strood

5
你好, 声明
static uint8_t l_matrix[10][20];

已为10行20个uint8_t位置设置了存储空间,即200个uint8_t大小的位置,每个元素可通过计算20 x 行 + 列来找到。

所以不会

uint8_t (*matrix_ptr)[20] = l_matrix;

能否给你所需的东西并指向数组第一行的零列元素?

编辑:进一步思考,一个数组名字不是定义为指针吗?也就是说,数组的名称是第一个元素的位置的同义词,即l_matrix[0][0]?

编辑2:正如其他人所提到的,评论空间对于进一步讨论来说有点太小了。无论如何:

typedef uint8_t array_of_20_uint8_t[20];
array_of_20_uint8_t *matrix_ptr = l_matrix;

该语句并未为所涉及的数组提供任何存储分配。

如上所述,根据标准定义,该语句为:

static uint8_t l_matrix[10][20];

已经设置了200个类型为uint8_t的连续位置。

通过以下形式的语句引用l_matrix:

(*l_matrix + (20 * rowno) + colno)

将为您提供在第rowno行找到的第colno个元素的内容。

所有指针操作都会自动考虑所指对象的大小。- K&R第5.4节,第103页

如果在存储手头对象时涉及填充或字节对齐偏移,则情况也是如此。编译器将自动调整这些内容。根据C ANSI标准的定义。

希望能帮到您。

祝好,


1
uint8_t (*matrix_ptr)[][20],第一个括号应该省略,正确的写法是 uint8_t (*matrix_ptr)[20]。 - Aconcagua

5

在C99中(由clang和gcc支持),有一种通过引用传递多维数组给函数的模糊语法:

int l_matrix[10][20];

void test(int matrix_ptr[static 10][20]) {
}

int main(void) {
    test(l_matrix);
}

与普通指针不同,该指针提供了关于数组大小的提示,理论上允许编译器警告传递的数组过小,并发现明显的越界访问问题。
遗憾的是,它不能解决sizeof()的问题,而且编译器似乎还没有使用这些信息,所以它仍然只是一个好奇心。

1
这个答案是误导性的:那不会使参数成为一个固定大小的数组,它仍然是一个指针。 static 10 是某种保证,至少有10个元素存在,这意味着大小并不是固定的。 - bluss
1
@bluss 这个问题是关于指针的,所以我不明白用指针(注意*按引用传递)回答为什么会误导。从函数的角度来看,数组的大小是固定的,因为访问超出这些边界的元素是未定义的。 - Kornel
我认为超过10的访问并不是未定义的,我没有看到任何指示。 - bluss
这个答案似乎暗示了如果没有关键字 static,数组就不会被按引用传递,但这是不正确的。数组总是被按引用传递的。原始问题询问了一个不同的用例 - 在同一函数/命名空间中使用补充指针访问2D数组的元素。 - Palo

4
您可以通过将数组声明为线性数组,并自己进行(行,列)到数组索引的计算来避免与编译器打交道。
static uint8_t l_matrix[200];

void test(int row, int col, uint8_t val)

{

   uint8_t* matrix_ptr = l_matrix;
   matrix_ptr [col+y*row] = val; // to assign a value

}

这其实就是编译器本来要做的事情。


1
这正是C编译器所做的事情。C实际上没有任何真正的“数组”概念——[]符号只是指针算术的语法糖。 - Ken Keenan
7
这种解决方案的缺点是永远无法找到正确的方法来完成它。 - Craig McQueen

2

指向多维数组的指针的基本语法是:

type (*pointer)[第一维大小][第二维大小][..] = &array_name

调用它的基本语法是:

(*pointer_name)[1st index][2nd index][...]

这是一个例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
   // The multidimentional array...
   char balance[5][100] = {
       "Subham",
       "Messi"
   };

   char (*p)[5][100] = &balance; // Pointer initialization...

   printf("%s\n",(*p)[0]); // Calling...
   printf("%s\n",(*p)[1]); // Calling...

  return 0;
}

输出结果为:

Subham
Messi

它起作用了...


1
您想要一个指向第一个元素的指针,因此;
static uint8_t l_matrix[10][20];

void test(){
   uint8_t *matrix_ptr = l_matrix[0]; //wrong idea 
}

1
你可以这样做:
uint8_t (*matrix_ptr)[10][20] = &l_matrix;

1
这不会占用10 * 20字节的RAM吗?(我在微控制器上) - Dill
它将占用4个字节,或者您的计算机指针大小所能容纳的任何大小。但是请记住,如果您拥有它,您必须使用matrix_ptr[0][x][y]或(*matrix_ptr)[x][y]进行索引。这是“指向二维数组”的直接逐字翻译:p - Johannes Schaub - litb
谢谢 litb,我忘了提到如何访问它。既然你的答案做得很好,编辑我的回答没有意义 :) - Nick Dandoulakis
那么,这个占用了 10 * 20 字节的 RAM 吗? - Danijel
@Danijel,由于它是指向二维数组的指针,因此它只会占用4个字节或者在你的计算机中指针的大小,即16位、32位、64位等。 - Nick Dandoulakis
好的,谢谢。调试器让我感到困惑,显示了一些内存使用情况。 - Danijel

0
您还可以添加偏移量,如果要使用负索引:
uint8_t l_matrix[10][20];
uint8_t (*matrix_ptr)[20] = l_matrix+5;
matrix_ptr[-4][1]=7;

如果您的编译器出现错误或警告,您可以使用以下方法:

uint8_t (*matrix_ptr)[20] = (uint8_t (*)[20]) l_matrix;

你好。这个问题被标记为 c,因此答案应该使用相同的语言。请注意标签。 - 2501

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接