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

157

这个问题是针对 C 专家的:

在 C 中,可以按照以下方式声明一个指针:

char (* p)[10];

这基本上表示该指针指向一个由10个字符组成的数组。使用这种方式声明指针的好处是,如果你试图将不同大小的数组指针分配给p,则会在编译时出错。它还会在您尝试将简单字符指针的值分配给p时导致编译时错误。我用gcc尝试过,并且似乎与ANSI、C89和C99兼容。

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

void foo(char * p, int plen);

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

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

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

这似乎非常有用,但我从来没有在我运行的任何代码中看到过像这样声明指针的方式。

我的问题是:为什么人们不这样声明指针?我是否忽略了一些显而易见的陷阱?


3
请注意:自 C99 以来,数组无需像标题所建议的那样具有固定大小,10 可以被作用域内的任何变量替换。 - M.M
10个回答

207

你在帖子中所说的是绝对正确的。我认为每个 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 无关。您可以随时从数组切换到 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开发人员来说过于复杂,这就是为什么您在“典型”的日常代码中很少看到它的原因。


4
这个答案非常简明扼要地描述了sizeof()函数如何成功,以及它通常如何失败和总是失败的方法。您对大多数C/C++工程师不理解,因此他们会做他们认为自己理解的事情的观察是我最近看到的更有预见性的事情之一,而这种观点所描述的准确性无可比拟。真的,先生,回答得非常好。 - WhozCraig
1
我很好奇您如何使用这种技术处理const属性。 一个 const char (*p)[N] 参数似乎与 char table[N] 的指针不兼容; 相比之下,一个简单的 char* 指针仍然与 const char* 参数兼容。 - Cyan
8
值得注意的是,要访问数组的元素,你需要执行(*p)[i]而不是*p[i]。后者会跳过数组的大小,这几乎肯定不是你想要的。至少对于我来说,学习这种语法引起了错误,而不是预防它;只需传递一个float*指针就可以更快地得到正确的代码。 - Andrew Wagner
2
是的 @mickey,你建议的是一个指向可变元素数组的const指针。 而且,这与指向不可变元素数组的指针完全不同。 - Cyan
2
@Smiley1000:啥?你正在从g中调用g。你还没有测试任何东西。这里是一个真正的版本的测试:https://godbolt.org/z/doYhGnsxq - AnT stands with Russia
显示剩余4条评论

15

我想在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规范(在我的版本中是第9页...讨论两个限定类型兼容的部分),并同意它似乎表明这些转换是非法的。然而,我们知道在实践中,您始终可以自动将char *转换为char const *而不会收到警告。在我的看法中,clang比gcc更一致地允许这种情况,尽管我同意您的观点,即规范似乎禁止任何这些自动转换。 - Doug Richardson
GCC 喜欢它。 - Smiley1000
1
@Smiley1000:不,它没有:https://godbolt.org/z/xsdhd97dh。这个问题计划在C23中解决。但目前它仍然存在于语言中。 - AnT stands with Russia

3

我也希望使用这种语法来实现更多的类型检查。

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

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

  • Accessing the array requires using (*p)[]:

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

    It is tempting to use a local pointer-to-char instead:

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

    But this would partially defeat the purpose of using the correct type.

  • One has to remember to use the address-of operator when assigning an array's address to a pointer-to-array:

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

    The address-of operator gets the address of the whole array in &a, with the correct type to assign it to p. Without the operator, a is automatically converted to the address of the first element of the array, same as in &a[0], which has a different type.

    Since this automatic conversion is already taking place, I am always puzzled that the & is necessary. It is consistent with the use of & on variables of other types, but I have to remember that an array is special and that I need the & to get the correct type of address, even though the address value is the same.

    One reason for my problem may be that I learned K&R C back in the 80s, which did not allow using the & operator on whole arrays yet (although some compilers ignored that or tolerated the syntax). Which, by the way, may be another reason why pointers-to-arrays have a hard time to get adopted: they only work properly since ANSI C, and the & operator limitation may have been another reason to deem them too awkward.

  • When typedef is not used to create a type for the pointer-to-array (in a common header file), then a global pointer-to-array needs a more complicated extern declaration to share it across files:

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

3
显而易见的原因是这段代码无法编译:
extern void foo(char (*p)[10]);
void bar() {
  char p[10];
  foo(p);
}

数组的默认升级为未限定指针。

还可以参见这个问题,使用foo(&p)应该可以工作。


3
当然,foo(p) 不会起作用,因为 foo 要求一个指向包含 10 个元素的数组的指针,所以你需要传递你的数组的地址... - Brian R. Bondy
12
“显而易见的原因”是什么?显然,正确的调用函数的方式是 foo(&p) - AnT stands with Russia
4
我想“obvious”这个词用得不对,我是指“最直接的”。在这种情况下,p和&p之间的区别对于普通的C程序员来说相当难理解。有人试图按照帖子中的建议去完成任务,会写出我写的代码,然后会在编译时遇到错误而放弃。 - Keith Randall

1

简单来说,C语言不是这样做的。类型为T的数组被传递时,只会传递指向数组中第一个T的指针。

这种方式可以实现一些很酷、优雅的算法,例如使用表达式遍历数组:

*dst++ = *src++

缺点是大小的管理取决于您。不幸的是,没有认真对待这一点也导致了 C 代码中数百万个错误和/或恶意利用机会。

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

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

尽管如此,没有什么能阻止您或不称职或恶意的程序员使用强制转换来欺骗编译器,以便将您的结构体视为不同大小的结构体之一。这种几乎无拘束的能力是 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 stands with Russia
请点击此处查看有关char array[]和char *ptr之间差异的更详细讨论。https://dev59.com/YXI-5IYBdhLWcg3wiY3a#1807766 - t0mm13b

1

我不建议使用这个解决方案

typedef int Vector3d[3];

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

void foo(Vector3d a) {
   Vector3d b;
}

当a的大小不等于b的大小时


他并不是在建议这个作为一个解决方案。他只是拿它作为一个例子。 - figurassa
嗯,为什么 sizeof(a)sizeof(b) 不一样呢? - sherrellbc
@sherrellbc 因为数组作为函数参数时会隐式地衰减为指向它们的指针,所以这基本上等同于 void foo(Vector3d *a) - Smiley1000
1
你在声明中有一个拼写错误(应该是小写的3d)。 - Allan Wind

0
也许我漏掉了什么,但是......由于数组是常量指针,基本上意味着没有必要传递它们的指针。
你难道不能只使用 void foo(char p[10], int plen); 吗?

2
就这里所涉及的问题(将一维数组作为参数),事实上是它们会衰变成常量指针。请阅读有关如何减少迂腐的常见问题解答。 - fortran

0
 type (*)[]; 
// points to an array e.g
int (*ptr)[5];
// points to an 5 integer array
// gets the address of the array

type *[];
// points to an array of pointers e.g
int* ptr[5]
// point to an array of five integer pointers
// point to 5 adresses.

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

3
错误。它在vs2008、vs2010和gcc上都能正常工作。特别是这个例子可以良好地运行:https://dev59.com/eHfZa4cB1Zd3GeqPUKN2#19208364 - kotlomoy

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