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

C pointers : pointing to an array of fixed size

这个问题在那里向C大师们提出:

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

1
char (* p)[10];

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

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

1
void foo(char * p, int plen);

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

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

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

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

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


你在帖子中说的话绝对正确。我会说,每个C开发人员都会得到完全相同的发现,并且当(如果)他们达到一定的C语言熟练程度时得出完全相同的结论。

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

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

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

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

)。

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

1
2
3
4
5
6
7
8
typedef int Vector3d[3];

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

另外注意,上述代码对于作为数组的Vector3d类型或struct是不变的。您可以随时将Vector3d的定义从数组切换到struct并返回,您不必更改函数声明。在任何一种情况下,函数都将"通过引用"接收聚合对象(对此有例外,但在本讨论的上下文中这是真的)。

但是,你不会经常过多地使用这种数组传递方法,因为太多人会因为语法相当复杂而感到困惑,并且对C语言的这些功能使用它们感到不舒服。出于这个原因,在平均现实生活中,将数组作为指向其第一个元素的指针传递是一种更流行的方法。它看起来"更简单"。

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

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

实际上,在许多情况下,能够处理运行时大小的数组非常有用,这也有助于该方法的流行。许多C开发人员根本不会遇到(或永远不会认识到)处理固定大小数组的需要,因此对于正确的固定大小技术仍然无知。

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

1
void foo(char p[])

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

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

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

此数组不能传递给声明为的函数

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

这使得普通开发人员感到困惑,并使他们放弃了固定大小的参数声明,而没有进一步考虑。但实际上,问题的根源在于天真的malloc方法。上面显示的malloc格式应保留给运行时大小的数组。如果数组类型具有编译时大小,则malloc的更好方法如下所示

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

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

1
foo(p);

并且编译器将执行正确的类型检查。但同样,这对于一个毫无准备的C开发人员来说过于混乱,这就是为什么你不会经常在"典型的"日常平均代码中看到它。


我想补充一下AndreyT的答案(万一有人在这个页面上发现了关于这个主题的更多信息):

当我开始更多地使用这些声明时,我意识到在C中存在与它们相关的主要障碍(显然不是在C ++中)。通常情况下,您希望为调用者提供指向您已写入的缓冲区的const指针。不幸的是,当在C中声明这样的指针时,这是不可能的。换句话说,C标准(6.7.3 - 第8段)与这样的事情不一致:

1
2
3
   int array[9];

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

这种约束似乎不存在于C ++中,使得这些类型的声明更加有用。但是在C的情况下,每当你想要一个指向固定大小缓冲区的const指针时,就必须回退到常规指针声明(除非缓冲区本身被声明为const开始)。您可以在此邮件主题中找到更多信息:链接文本

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


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

1
2
3
4
5
extern void foo(char (*p)[10]);
void bar() {
  char p[10];
  foo(p);
}

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

另请参阅此问题,使用foo(&p)应该可行。


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

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

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

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

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

    很有可能使用本地指针到char:

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

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

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

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

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

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

    我的问题的一个原因可能是我在80年代学习了K&R C,但是不允许在整个阵列上使用&运算符(尽管有些编译器忽略了这一点或者容忍了语法)。顺便说一下,这可能是指针到阵列很难被采用的另一个原因:它们只能在ANSI C中正常工作,而&运算符限制可能是另一个认为它们太尴尬的原因。

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

    1
    2
    3
    4
    5
    fileA:
    char (*p)[10];

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

我不推荐这个解决方案

1
typedef int Vector3d[3];

因为它模糊了Vector3D具有你的类型这一事实
必须知道。程序员通常不会期望变量
相同类型有不同的尺寸。考虑:

1
2
3
void foo(Vector3d a) {
   Vector3D b;
}

sizeof a!= sizeof b


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

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

按值获取数组的函数的原型是:

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

或参考:

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

或者通过数组语法:

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


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

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

1
*dst++ = *src++

缺点是管理尺寸取决于您。遗憾的是,未能认真做到这一点也导致数百万的C编码错误和/或恶意利用的机会。

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

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

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


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

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


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