在C语言中,使用第一个数组元素作为数组长度是一种好的编程实践吗?

11

因为在C语言中,数组长度必须在定义数组时声明,那么使用第一个元素作为长度是否是可接受的做法?例如:

int arr[9]={9,0,1,2,3,4,5,6,7};

然后使用这样的一个函数来处理数组:

int printarr(int *ARR) {
    for (int i=1; i<ARR[0]; i++) {
        printf("%d ", ARR[i]);
    }
} 

我认为这没有什么问题,但我更愿意先向有经验的C程序员进行核实。我将是唯一使用该代码的人。


8
简而言之:不行。如果数组的类型不是数字,怎么办? - Andrew Henle
1
这真的取决于情况。很难做出一般性的陈述。如果您编写的代码像这样,至少应该在注释中进行适当的文档化。 - Jabberwocky
1
如果数组是像您的示例中的固定大小数组,您可以简单地执行以下操作:#define MYSIZE 9,然后 int arr[MYSIZE]={0,1,2,3,4,5,6,7};。现在您知道大小为MYSIZE。否则,固定大小数组的大小始终为sizeof(array) / sizeof(array[0])。一些实现具有_countof宏,如果没有,您可以基于上述内容自己制作。 - Jabberwocky
8
我喜欢这个问题。虽然这是一个坏主意,但这是一个好问题。 - klutt
非常感谢您所有出色的评论和建议。我现在会按照推荐使用结构体,包括数组、长度和附加描述。我认为我倾向于在数组中包含元数据,使用了 C 的俚语版本。 - Ian Stewart
7个回答

12

从某种意义上来说,这很糟糕,因为你有一个数组,其中元素的含义不同。将元数据与数据存储在一起并不是一件好事。只是稍微推广一下你的想法。我们可以使用第一个元素表示元素大小,然后使用第二个元素表示长度。试着编写一个利用两者的函数 ;)

值得注意的是,使用这种方法,如果数组大于元素可容纳的最大值,对于char数组来说是非常重要的限制,你会遇到问题。当然,你可以通过使用前两个元素来解决它。如果你有浮点数数组,你也可以使用强制类型转换。但我可以保证,你会因此遇到难以追踪的错误。其中,字节序可能会引起很多问题。

这肯定会让几乎所有经验丰富的C程序员都感到困惑。这并不是真正反对这个想法本身的逻辑论据,而是一种实用主义的论据。即使这是一个好主意(它不是),你也必须和每一个将要涉及你的代码的程序员进行长时间的谈话。

实现相同功能的一个合理方式是使用结构体。

struct container {
    int *arr;
    size_t size;
};

int arr[10];

struct container c = { .arr = arr, .size = sizeof arr/sizeof *arr };

但是在我会使用类似上述代码的任何情况下,我可能不会使用数组,而是使用动态分配:

const size_t size = 10;
int *arr = malloc(sizeof *arr * size);
if(!arr) { /* Error handling */ }

struct container c = { .arr = arr, .size = size };

需要注意的是,如果您使用指针而不是数组进行初始化,可能会得到“有趣”的结果。

您也可以使用灵活数组,就像Andreas在他的答案中所写的一样。


1
@Cosinus 这是个人口味的问题。我还没有真正下定决心。使用括号会让它看起来像一个函数,但实际上它是一个操作符。 - klutt
到目前为止,我从来没有看到过使用类型名称而不是变量名作为 sizeof 的参数的原因。 - klutt
1
@Cosinus 做过多的事情“只是为了安全起见”通常表明不知道具体细节,即缺乏对 语法 的完全理解。类型需要用括号表示,表达式则不需要。表达式遵循运算符优先级。不存在“可能不起作用”的情况。 - Oka
@Oka,我知道表达式遵循运算符优先级,但是这种优先级并不总是相同的。它取决于编译器。当然,当涉及到+-*/时,它总是会以相同的方式工作,但是当涉及到二元操作符例如那些不经常使用的(|^)时,一些编译器将会有所不同。我可以想象,对于一些编译器,sizeof a / sizeof b可能会被解释为sizeof(a / sizeof(b)) - Cosinus
1
一个将其解释为 sizeof(a / sizeof(b)) 的编译器不符合标准。 - klutt
显示剩余4条评论

6

在C语言中,你可以使用灵活数组成员。也就是说,你可以编写:

struct intarray {
   size_t count;
   int data[];  // flexible array member needs to be last
};

您可以使用以下方式进行内存分配:

size_t count = 100;
struct intarray *arr = malloc( sizeof(struct intarray) + sizeof(int)*count );
arr->count = count;

这可以用于所有类型的数据。

它使得使用C数组更安全(不如C++容器安全,但比普通的C数组更安全)。

不幸的是,C++标准不支持这种惯用法。许多C++编译器提供它作为扩展,但不能保证。

另一方面,与C++容器不同,这个C FLA习惯用法可能更加显式和高效,因为它不需要额外的间接寻址和/或需要两个分配(想想new vector<int>)。

如果你坚持使用C,我认为这是一种非常显式和易读的处理带有集成大小的可变长度数组的方式。

唯一的缺点是C++的人不喜欢它,而更喜欢C++容器。


2
如果您提到一个结构体最多只能有一个柔性数组成员,并且它必须是结构体中的最后一个,那就太好了。 - klutt
@klutt 谢谢,我已经添加了一条评论。 - Andreas H.
这也是Windows的常见习语(带有一些兼容性警告,另外据我所知,在c99中0长度数组也不合法,所以要小心-但总体观点仍然成立)。除此之外,我习惯使用offsetof()sizeof是否保证处理填充? - Voo
@Voo sizeof "处理填充" - 相反 - 它经常是过多的 - Antti Haapala -- Слава Україні
也许只需要使用 sizeof *arr + sizeof arr->data[0] * count :D - Antti Haapala -- Слава Україні

1

如果数组的元素是整数,那么它不会出现未定义行为或其他可移植性问题,但是你应该让它计算数组长度,而不是直接写魔法数字9,以避免打错字。

#include <stdio.h>

int main(void) {
    int arr[9]={sizeof(arr)/sizeof(*arr),0,1,2,3,4,5,6,7};
    
    for (int i=1; i<arr[0]; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

好吧,这是用更好的方式做一件非常糟糕的事的方法 :) - klutt

1
它适用于相对有限的情况。解决它所涉及的问题有更好的方案。其中一个问题是,如果它不被普遍应用,那么你会有一些使用约定和一些没有使用的数组 - 你无法确定一个数组是否使用了该约定。例如,对于用于携带字符串的数组,你必须在调用标准字符串库时不断传递&arr[1],或者定义一个使用“Pascal字符串”而不是“ASCIZ字符串”约定的新字符串库(这样的库实际上更有效率)。对于真正的数组而不仅仅是指向内存的指针,sizeof(arr) / sizeof(*arr)将产生元素数量,而不需要在任何情况下将其存储在数组中。它只适用于整数类型数组,并且对于char数组,长度会受到限制。对于其他对象类型或数据结构的数组,它并不实用。更好的解决方案是使用一个结构体:
typedef struct
{
    size_t length ;
    int* data ;
} intarray_t ;

然后:

int data[9] ;
intarray_t array{ sizeof(data) / sizeof(*data), data } ;

现在您有一个数组对象,可以传递给函数并保留大小信息,数据成员可以直接访问,以便在不接受的第三方或标准库接口中使用。此外,data成员的类型可以是任何类型。

请注意,当涉及到托管系统时,灵活数组成员版本可能是更优雅的解决方案,而在没有堆的嵌入式系统中,本答案是首选。 - Lundin

1

只有少数数据类型适合这种黑客方式。因此,我建议不要这样做,因为这会导致在不同类型的数组中实现风格不一致。


1

在字符缓冲区中,很常用一种类似的方法,在缓冲区的开头存储实际长度。

C 语言中的动态内存分配也使用了这种方法,即分配的内存以一个整数为前缀,保存了所分配内存的大小。

但是通常情况下,这种方法对数组不适用。例如,字符数组可能比类型为 char 的对象可以存储的最大正值(127)要大得多。此外,将这样的数组的子数组传递给函数也很困难。大多数设计用于处理数组的函数在这种情况下都无法工作。

处理数组的通用方法是声明两个参数的函数。第一个参数具有指针类型,指定数组或子数组的初始元素,第二个参数指定数组或子数组的元素数量。

C 还允许声明接受可变长度数组的函数,当它们的大小可以在运行时指定时。


0

显然答案是否定的。所有编程语言都有预定义的函数与变量类型一起存储。为什么不使用它们呢?对于你的情况,更适合访问计数/长度方法而不是测试第一个值。

if语句有时比预定义函数需要更多的时间。

乍一看,存储计数器似乎没问题,但想象一下你将不得不更新数组。您将需要执行两个操作,一个插入另一个更新计数器。因此,2个操作意味着必须更改2个变量。对于静态数组,可能将它们作为计数器与列表一起使用,但对于动态数组,则否定否定否定。 另一方面,请阅读编程基本概念,您会发现您的想法是错误的,不符合编程原则。


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