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

Posted

技术标签:

【中文标题】C指针:指向一个固定大小的数组【英文标题】:C pointers : pointing to an array of fixed size 【发布时间】:2010-12-21 01:43:51 【问题描述】:

这个问题发给了 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]);

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

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

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

【问题讨论】:

注意:由于 C99 数组不必像标题所建议的那样具有固定大小,10 可以被范围内的任何变量替换 【参考方案1】:

您在帖子中所说的绝对正确。我想说每个 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 语言的这些特性不够满意,无法正确使用它们。出于这个原因,在现实生活中,将数组作为指向其第一个元素的指针传递是一种更流行的方法。它只是看起来“更简单”。

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

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 开发人员来说过于混乱,这就是为什么您不会在“典型”的普通日常代码中经常看到它。

【讨论】:

答案提供了关于 sizeof() 如何成功、如何经常失败以及它总是失败的方式的非常简洁和翔实的描述。你对大多数 C/C++ 工程师不理解的观察,因此做他们认为他们理解的事情,这是我一段时间以来看到的更具预言性的事情之一,与它所描述的准确性相比,面纱微不足道。说真的,先生。很好的答案。 我刚刚根据这个答案重构了一些代码,非常感谢 Q 和 A。 我很想知道您如何使用这种技术处理const 属性。 const char (*p)[N] 参数似乎与指向 char table[N]; 的指针不兼容,相比之下,简单的 char* ptr 仍然与 const char* 参数兼容。 请注意,要访问数组的元素,您需要使用(*p)[i] 而不是*p[i],这可能会有所帮助。后者会随着数组的大小而跳跃,这几乎肯定不是你想要的。至少对我来说,学习这种语法会导致而不是防止错误。只要传递一个浮点数*,我就能更快地获得正确的代码。 是的@mickey,你建议的是const 指向可变元素数组的指针。是的,这与指向不可变元素数组的指针完全不同。【参考方案2】:

我想补充一下 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 开头)。您可以在此邮件线程中找到更多信息:link text

在我看来这是一个严重的限制,这可能是人们通常不在 C 中声明这样的指针的主要原因之一。另一个是大多数人甚至不知道你可以声明指针的事实正如 AndreyT 指出的那样。

【讨论】:

这似乎是编译器特定的问题。我能够使用 gcc 4.9.1 进行复制,但是 clang 3.4.2 能够从非 const 版本转到 const 版本没问题。我确实阅读了 C11 规范(我的版本中的第 9 页......关于两种合格类型兼容的部分)并同意它似乎说这些转换是非法的。但是,我们知道在实践中您始终可以自动从 char* 转换为 char const* 而不会发出警告。 IMO,clang 在允许这一点方面比 gcc 更一致,尽管我同意你的观点,规范似乎禁止任何这些自动转换。【参考方案3】:

明显的原因是这段代码无法编译:

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

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

另请参阅this question,使用foo(&p) 应该可以工作。

【讨论】:

当然 foo(p) 不起作用,foo 要求一个指向 10 个元素的数组的指针,因此您需要传递数组的地址... “显而易见的原因”是怎么回事?显然,调用函数的正确方法是foo(&p) 我猜“明显”是错误的词。我的意思是“最直接”。在这种情况下,p 和 &p 之间的区别对于普通的 C 程序员来说是非常模糊的。如果有人试图按照海报的建议去做,就会写出我写的东西,得到一个编译时错误,然后放弃。【参考方案4】:

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

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

这是我遇到的更多障碍。

访问数组需要使用(*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;

但这会部分破坏使用正确类型的目的。

在将数组的地址分配给指向数组的指针时,必须记住使用地址运算符:

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

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

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

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

typedef用于为指向数组的指针创建类型(在通用头文件中),那么全局指向数组的指针需要更复杂的@987654335 @声明跨文件共享:

fileA:
char (*p)[10];

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

【讨论】:

【参考方案5】:

嗯,简单地说,C 不会那样做。 T 类型的数组作为指向数组中第一个 T 的指针传递,这就是你得到的全部。

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

*dst++ = *src++

缺点是大小的管理取决于您。不幸的是,未能认真地做到这一点也导致了 C 编码中的数百万错误,和/或恶意利用的机会。

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

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

不过,没有什么可以阻止您或不称职或恶意的编码人员使用强制转换来欺骗编译器,将您的结构视为不同大小的结构。做这种事情的几乎不受束缚的能力是 C 设计的一部分。

【讨论】:

【参考方案6】:

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

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*

【讨论】:

别忘了固定大小的数组也可以动态分配为char (*p)[10] = malloc(sizeof *p) 这里查看 char array[] 和 char *ptr 的区别的更详细的讨论。 ***.com/questions/1807530/…【参考方案7】:

我不会推荐这个解决方案

typedef int Vector3d[3];

因为它掩盖了 Vector3D 有一个类型的事实 必须知道。程序员通常不期望变量 相同的类型有不同的尺寸。考虑:

void foo(Vector3d a) 
   Vector3D b;

其中 sizeof a != sizeof b

【讨论】:

他并没有建议将此作为解决方案。他只是以此为例。 嗯。为什么sizeof(a)sizeof(b) 不一样?【参考方案8】:

也许我遗漏了一些东西,但是...由于数组是常量指针,基本上这意味着将指针传递给它们是没有意义的。

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

【讨论】:

对于这里重要的(一维数组作为参数),事实是它们衰减为常量指针。请阅读有关如何不那么迂腐的常见问题解答。【参考方案9】:

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

【讨论】:

-1 错误。它在 vs2008、vs2010、gcc 上运行良好。特别是这个例子工作正常:***.com/a/19208364/2333290

以上是关于C指针:指向一个固定大小的数组的主要内容,如果未能解决你的问题,请参考以下文章

C 语言字符串 一级指针 内存模型 ( 指定大小字符数组 | 未指定大小字符数组 | 指向常量字符串的指针 | 指向堆内存的指针 )

如何在 C++ 中分配一个二维指针数组

C语言中关于二维数据指针的问题?

C 语言二级指针案例 ( 字符串切割 | 返回 自定义二级指针 作为结果 | 每个 一级指针 指向不同大小内存 | 精准分配每个 一级指针 指向的内存大小 )

《C陷阱和缺陷》读书笔记-其二:语义陷阱

指向固定大小数组行为的指针