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


118

这个问题出现在C语言专家那里:

在C中,可以如下声明一个指针:

char (* p)[10];

..基本上表明该指针指向10个字符的数组。像这样声明一个指针的整洁之处在于,如果尝试为p分配一个大小不同的数组的指针,则会出现编译时错误。如果您尝试将简单的char指针的值分配给p,也会给您带来编译时错误。我在gcc上尝试过,似乎可以在ANSI,C89和C99上使用。

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

void foo(char * p, int plen);

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

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

..将强制调用方为您提供指定大小的缓冲区。

这似乎很有用,但我从未在任何遇到过的代码中看到过像这样声明过的指针。

我的问题是:人们为什么不声明这样的指针有什么理由?我没有看到明显的陷阱吗?


3
注意:由于C99阵列不必是固定大小的由标题的建议,10可以通过任何可变范围被替换
MM

Answers:


172

您在帖子中所说的是绝对正确的。我想说的是,每位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);

另外请注意,上述代码对于Vector3dtype是数组还是a 是不变的struct。您可以Vector3d随时将数组的定义从a 切换到a 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开发人员带来了太多混乱,这就是为什么您不会在“典型”的日常代码中经常看到它的原因。


2
答案对sizeof()如何成功,如何经常失败以及总是失败的方式进行了非常简洁和有益的描述。您对大多数C / C ++工程师的了解没有理解,因此,做他们认为自己可以理解的事情是我一段时间内看到的比较预言的事情之一,而与它描述的准确性相比,这是一无是处。先生 好答案。
WhozCraig 2012年

我只是基于此答案重构了一些代码,赞扬并感谢Q和
Perry

1
我很好奇您如何const使用此技术处理财产。一个const char (*p)[N]说法似乎并不具有指向兼容char table[N];与此相反,一个简单的char*PTR保持与兼容的const char*论点。
Cyan

4
可能需要注意的是,要访问数组的元素,您需要这样做,(*p)[i]而不是*p[i]。后者将以数组的大小跳跃,几乎可以肯定这不是您想要的。至少对我来说,学习这种语法会导致而不是避免出现错误。我只是通过一个float *就可以更快地得到正确的代码。
安德鲁·瓦格纳

1
是的@mickey,您建议使用的是const指向可变元素数组的指针。是的,这与指向不可变元素数组的指针完全不同。
青色

11

我想补充一下AndreyT的答案(以防万一有人偶然在此页面上寻找有关此主题的更多信息):

当我开始更多地使用这些声明时,我意识到C中与它们相关的主要障碍(显然在C ++中没有)。在某些情况下,您想给调用者一个指向已写入缓冲区的const指针是很常见的。不幸的是,当在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指针时,都必须回退到常规指针声明(除非缓冲区本身被声明为const开头)。您可以在此邮件主题中找到更多信息:链接文本

在我看来,这是一个严格的约束,这可能是人们通常不使用C声明这样的指针的主要原因之一。另一个事实是,大多数人甚至都不知道您可以这样声明一个指针: AndreyT指出。


2
这似乎是编译器特有的问题。我可以使用gcc 4.9.1复制,但是clang 3.4.2可以从非const转换为const版本没问题。我确实阅读了C11规范(在我的版本中为p 9,该部分谈论的是两种合格类型的兼容性),并同意看来这些转换是非法的。但是,实际上我们知道您总是可以在没有警告的情况下自动从char *转换为char const *。IMO,与gcc相比,clang在允许这样做方面更加一致,尽管我同意您的观点,即规范似乎禁止其中的任何自动转换。
道格·理查森

4

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

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

数组的默认提升是指向不合格的指针。

也请参见此问题,使用foo(&p)应该有效。


3
当然foo(p)将不起作用,foo需要一个指向10个元素的数组的指针,因此您需要传递数组的地址...
Brian R. Bondy

8
这是“显而易见的原因”吗?显然,可以理解调用函数的正确方法是foo(&p)
AnT

3
我猜“明显”是错误的词。我的意思是“最直接”。在这种情况下,p和&p之间的区别对于普通C程序员而言是相当模糊的。尝试执行发布者建议的操作的人会写我写的东西,遇到编译时错误,然后放弃。
基思·兰德尔

1

简而言之,C不会那样做。类型数组T作为指针传递给T数组中的第一个指针,仅此而已。

这允许使用一些很酷而优雅的算法,例如使用以下表达式在数组中循环

*dst++ = *src++

缺点是大小的管理取决于您。不幸的是,不认真执行此操作还导致了C编码中的数百万个错误和/或恶意利用的机会。

与您在C语言中所要求的最接近的是传递一个struct(按值)或一个指向一个(按引用)的指针。只要在此操作的两边都使用相同的结构类型,则分发引用的代码和使用引用的代码都将与要处理的数据大小一致。

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

但是,没有什么可以阻止您或能力不强或恶意的编码器使用强制转换来欺骗编译器以将您的结构视为不同大小的结构之一。C的设计几乎包括了完成这种事情的能力。


1

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

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*

2
不要忘记,固定大小的数组也可以动态分配为char (*p)[10] = malloc(sizeof *p)
AnT

请参见此处,以获得有关char array []和char * ptr之间差异的更详细讨论。stackoverflow.com/questions/1807530/…–
t0mm13b

1

我不推荐这种解决方案

typedef int Vector3d[3];

因为它掩盖了Vector3D具有您必须了解的类型这一事实。程序员通常不希望相同类型的变量具有不同的大小。考虑:

void foo(Vector3d a) {
   Vector3D b;
}

其中sizeof a!= sizeof b


他没有建议将此作为解决方案。他只是以此为例。
figurassa

嗯 为什么sizeof(a)不一样sizeof(b)
sherrellbc

1

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

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

这是我遇到的更多障碍。

  • 访问数组需要使用(*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],但类型不同。

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

    我遇到问题的原因之一可能是我在80年代就学习了K&R C,这还不允许&在整个数组上使用该运算符(尽管某些编译器忽略了这一点或容忍了这种语法)。顺便说一下,这可能是指针数组很难采用的另一个原因:它们仅在ANSI C之后才能正常工作,并且&运算符的限制可能是认为它们过于尴尬的另一个原因。

  • typedef用于创建指针到阵列的类型(在一个共同的头文件中),则全局指针到阵列需要一个更复杂的extern声明跨文件共享:

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

0

也许我缺少了一些东西,但是...由于数组是常量指针,从根本上讲,这意味着将指针传递给它们毫无意义。

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


4
数组不是常量指针。请阅读一些有关阵列的常见问题解答。
AnT

2
对于这里重要的事情(作为参数的一维数组),事实是它们会衰减为常量指针。请阅读有关如何减少书呆子的常见问题解答。
fortran

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.