C 指针: 指向一个固定大小的数组

这个问题要问那些 C 大师:

在 C 语言中,可以像下面这样声明一个指针:

char (* p)[10];

..基本上就是说这个指针指向一个包含10个字符的数组。像这样声明一个指针的好处是,如果试图将一个不同大小的数组的指针赋值给 p,就会得到一个编译时错误。如果您试图将一个简单的字符指针的值赋给 p,它也会给您一个编译时错误。我在 gcc 中尝试过这种方法,它似乎适用于 ANSI、 C89和 C99。

在我看来,像这样声明一个指针是非常有用的——特别是当传递一个指向函数的指针时。通常,人们会编写这样一个函数的原型:

void foo(char * p, int plen);

如果您期望一个特定大小的缓冲区,那么只需测试 plen 的值。但是,不能保证传递 p 给您的人真的会在该缓冲区中为您提供充足的有效内存位置。您必须相信调用这个函数的人正在做正确的事情。另一方面:

void foo(char (*p)[10]);

. . 会强迫调用者给你一个指定大小的缓冲区。

这似乎非常有用,但我从未见过任何代码中像这样声明的指针。

我的问题是: 为什么人们不像这样声明指针呢?我没看出什么明显的陷阱吗?

81848 次浏览

显而易见的原因是这段代码无法编译:

extern void foo(char (*p)[10]);
void bar() {
char p[10];
foo(p);
}

数组的默认提升是指向非限定指针。

另请参阅 这个问题,使用 foo(&p)应该可以。

也许我遗漏了一些东西,但是... 因为数组是常量指针,基本上这意味着没有必要传递指向它们的指针。

你就不能用 void foo(char p[10], int plen);吗?

简单来说,C 不是那样做事的。类型为 T的数组作为指向数组中第一个 T的指针传递,这就是所得到的全部内容。

这允许使用一些很酷很优雅的算法,比如使用下面的表达式循环遍历数组

*dst++ = *src++

缺点是规模的管理取决于你。不幸的是,如果不能认真地做到这一点,也会导致 C 代码中出现数百万个 bug,并且/或者出现恶意利用的机会。

类似于 C 语言要求的是传递一个 struct(通过值)或一个指向它的指针(通过引用)。只要在这个操作的两端使用相同的结构类型,传递引用的代码和使用引用的代码都会对所处理的数据的大小达成一致。

结构可以包含您想要的任何数据; 它可以包含定义明确的大小的数组。

尽管如此,没有什么能够阻止您或无能或恶意的编码者使用强制转换来欺骗编译器将结构视为不同大小的结构之一。几乎不受束缚的能力做这种事情是 C 的设计的一部分。

可以通过多种方式声明字符数组:

char p[10];
char* p = (char*)malloc(10 * sizeof(char));

通过值获取数组的函数的原型是:

void foo(char* p); //cannot modify p

或参考:

void foo(char** p); //can modify p, derefernce by *p[0] = 'f';

或者使用数组语法:

void foo(char p[]); //same as char*

你在帖子里说的完全正确。我想说的是,每个 C 开发人员在达到一定程度的 C 语言熟练程度时,都会得到完全相同的发现和完全相同的结论。

当应用程序区域的细节需要一个固定大小的数组(数组大小是一个编译时常量)时,将这样一个数组传递给函数的唯一正确方法是使用一个指针到数组的参数

void foo(char (*p)[10]);

(在 C + + 语言中,这也是通过引用完成的

void foo(char (&p)[10]);

).

这将启用语言级别的类型检查,以确保作为参数提供大小正确的数组。实际上,在许多情况下,人们在甚至没有意识到的情况下隐式地使用这种技术,将数组类型隐藏在 typedef 名称之后

typedef int Vector3d[3];


void transform(Vector3d *vector);
/* equivalent to `void transform(int (*vector)[3])` */
...
Vector3d vec;
...
transform(&vec);

另外请注意,上面的代码对于 Vector3d类型是数组还是 struct是不变的。您可以在任何时候将 Vector3d的定义从数组切换到 struct并返回,而且不必更改函数声明。无论哪种情况,函数都将“通过引用”接收聚合对象(这有例外,但在本讨论的上下文中,这是正确的)。

然而,您不会看到这种数组传递方法被过于频繁地显式地使用,这仅仅是因为太多的人被一种相当复杂的语法弄糊涂了,并且对 C 语言的这些特性感到不舒服,以至于无法正确地使用它们。因此,在平均实际生活中,将数组作为指向其第一个元素的指针传递是一种更流行的方法。只是看起来“更简单”。

但实际上,使用指向第一个元素的指针进行数组传递是一种非常小众的技巧,这种技巧服务于一个非常具体的目的: 它唯一的目的是方便传递 大小不同数组(即运行时大小)。如果您确实需要能够处理运行时大小的数组,那么传递这样一个数组的正确方法是通过指向其第一个元素的指针,该指针具有由附加参数提供的具体大小

void foo(char p[], unsigned plen);

实际上,在许多情况下,能够处理运行时大小的数组是非常有用的,这也有助于该方法的普及。许多 C 开发人员从来没有遇到(或者从来没有意识到)处理固定大小数组的需要,因此对适当的固定大小技术一无所知。

不过,如果数组大小是固定的,则将其作为指向元素的指针传递

void foo(char p[])

是一个重大的技术层面的错误,不幸的是,这是相当普遍的这些天。在这种情况下,指针到数组技术是一种更好的方法。

另一个可能阻碍采用固定大小的数组传递技术的原因是动态分配数组类型的天真方法占主导地位。例如,如果程序调用 char[10]类型的固定数组(如您的示例所示) ,一般开发人员将使用 malloc这样的数组

char *p = malloc(10 * sizeof *p);

不能将此数组传递给声明为

void foo(char (*p)[10]);

这会使一般的开发人员感到困惑,使他们放弃固定大小的参数声明,而不进一步考虑。但实际上,问题的根源在于幼稚的 malloc方法。上面显示的 malloc格式应该保留给运行时大小的数组。如果数组类型具有编译时大小,那么使用 malloc的更好方法如下所示

char (*p)[10] = malloc(sizeof *p);

当然,这可以很容易地传递给上面声明的 foo

foo(p);

编译器将执行正确的类型检查。但是,对于一个没有准备好的 C 开发人员来说,这又是一个过于令人困惑的问题,这就是为什么在“典型的”普通日常代码中不常见到它的原因。

我不推荐这种解决方案

typedef int Vector3d[3];

因为它掩盖了一个事实,那就是 Vector3D 有一个类型 程序员通常不期望 相同的类型有不同的尺寸。考虑:

void foo(Vector3d a) {
Vector3D b;
}

A 的大小 = b 的大小

我想补充一下 AndreyT 的回答(如果有人偶然发现这个页面,希望获得更多关于这个主题的信息) :

当我开始更多地使用这些声明时,我意识到在 C 中与它们相关的主要障碍(显然不是在 C + + 中)。这是相当常见的情况下,您希望给调用者一个常量指针到缓冲区,您已经写入。不幸的是,在 C 语言中像这样声明一个指针是不可能的。换句话说,C 标准(6.7.3-第8段)与以下内容不一致:


int array[9];


const int (* p2)[9] = &array;  /* Not legal unless array is const as well */

这个约束似乎不存在于 C + + 中,这使得这些类型的声明更加有用。但是对于 C 语言来说,只要需要一个指向固定大小缓冲区的常量指针(除非缓冲区本身从一开始就被声明为 const) ,就必须回到常规指针声明。你可以在这个邮件线程中找到更多的信息: 链接文本

在我看来,这是一个严重的约束,这可能是人们通常不会在 C 语言中像这样声明指针的主要原因之一。另一个原因是,大多数人甚至不知道可以像 AndreyT 指出的那样声明一个指针。

在我的编译器(vs2008)上,它将 char (*p)[10]视为一个字符指针数组,就好像没有括号一样,即使我将其编译为 C 文件也是如此。编译器是否支持这个“变量”?如果是这样,这就是不使用它的主要原因。

我还想使用这种语法来启用更多的类型检查。

但是我也同意使用指针的语法和思维模型更简单,更容易记忆。

以下是我遇到的一些障碍。

  • 访问数组需要使用 (*p)[]:

    void foo(char (*p)[10])
    {
    char c = (*p)[3];
    (*p)[0] = 1;
    }
    

    相反,使用本地指针到字符很有吸引力:

    void foo(char (*p)[10])
    {
    char *cp = (char *)p;
    char c = cp[3];
    cp[0] = 1;
    }
    

    但这将部分地违背使用正确类型的目的。

  • 将数组的地址分配给指针到数组时,必须记住使用 address-of 操作符:

    char a[10];
    char (*p)[10] = &a;
    

    Address-of 操作符获取 &a中整个数组的地址,并使用正确的类型将其分配给 p。如果没有操作符,a将自动转换为数组的第一个元素的地址,与 &a[0]相同,后者具有不同的类型。

    由于这种自动转换已经发生,我总是感到困惑的 &是必要的。这与在其他类型的变量上使用 &是一致的,但是我必须记住数组是特殊的,我需要 &来获得正确的地址类型,即使地址值是相同的。

    我的问题的一个原因可能是我在80年代学习了 K & R C,它还不允许在整个数组上使用 &操作符(尽管一些编译器忽略了这一点或者容忍了语法)。顺便说一句,这可能是指针到数组很难被采用的另一个原因: 它们只有在 ANSI C 之后才能正常工作,而 &操作符的限制可能是认为它们过于笨拙的另一个原因。

  • typedef没有用于为指针到数组(在公共头文件中)创建类型时,全局指针到数组需要更复杂的 extern声明来跨文件共享它:

    fileA:
    char (*p)[10];
    
    
    fileB:
    extern char (*p)[10];