将一个'typedef struct'数组传递给函数

4

I have the following situation:

file A.c:

typedef struct element
{
    uint16_t value_raw;
    float value_scaled;
    char *desc;
} element;

element sv[REG_READ_COUNT];

文件A.h:

typedef struct element element;

文件 B.c:

#include "A.h"
void dostuff (element sv[]) { } 

在编译时,我得到了一个错误:“error: array type has incomplete element type”,这是在B.c中函数参数定义的问题。

正确的方法是什么? 如何将类型为“element”的数组传递到函数中?


使用gcc+clang重现:http://coliru.stacked-crooked.com/a/e5e314deef461290 - Deduplicator
就此而言,bcc32 6.70也接受X *x并拒绝X x[] - M.M
这里有一个从未问过OP的问题,它实际上会影响到回答的方式。实际上有两种解决方案。一种涉及正确使用不透明(不完整)类型,而另一种则不需要。两种方法都是解决问题的有效方式。如果这是作业的一部分,并且要求您使用数据隐藏并在element周围创建抽象,则此处没有给出任何可接受的答案。如果不需要数据隐藏,则OP的选择是有道理的。因此,消除所有歧义的简单问题是 - 是否需要对element进行数据隐藏? - Michael Petch
结构体格式的定义应该在.h文件中。为了使结构体实例在任何地方都可见,实例声明需要在.h文件中。在您的代码中,实例声明在.h文件中,但结构体格式缺失。 - user3629249
5个回答

6
在文件B.c中,element是一个不完整的类型(它在A.h中未定义,只在A.c中定义)。C不允许使用不完整元素类型的数组声明符(正如您已经发现的那样)。这是来自C99草案的相关文本:

6.7.5.2 数组声明符

约束

  1. 除了可选类型限定符和关键字static之外,[]可以表示表达式或*。如果它们表示表达式(指定数组大小),则表达式应具有整数类型。如果表达式是常量表达式,则其值应大于零。元素类型不得是不完整或函数类型。可选类型限定符和关键字static只能出现在带有数组类型的函数参数声明中,并且仅出现在最外层的数组类型推导中。

这里强调一下,这适用于所有数组声明符,无论它们出现在哪里:变量声明、typedef、函数参数列表等。

要修复您的代码,请将完整的结构定义放入A.h中。或者,如果 dostuff 实际上不需要与元素一起工作(例如仅将"数组"传递到某些其他函数),则可以使用void dostuff(element *sv)


1
@Deduplicator:数组不是指针。如果元素的大小未知,则声明数组是非法的,据我所知,但声明指向未知元素的指针是合法的。你能否证明相反? - nneonneo
@nneonneo 他的意思是你可以传递一个指向元素结构体的指针数组。 - Michael Petch
@Deduplicator,没有未知大小的数组。有一个指向“element”的指针。 - juanchopanza
2
Gcc拒绝使用-std=c89-std=c99编译T t[]。我感到困惑,C11(n1570)6.7.6.3 p4中写道(在“约束”部分):_调整后,在函数声明符中的参数类型列表中,在该函数的定义的一部分中,不得具有不完整的类型。_据我理解,这个“调整后”是指上述p7 _将参数声明为“类型数组”的声明应调整为“类型的限定指针”,[...]_有趣。 - mafso
1
@Deduplicator:好的,各位,我已经修改了答案。你们的编译器没有问题,标准非常明确,数组声明中不完整的元素类型是不合法的。 - nneonneo
显示剩余11条评论

2

复现错误的最小代码。

struct element;
void dostuff (struct element sv[]) { } 

在coliru上使用clang和gcc进行测试:http://coliru.stacked-crooked.com/a/e5e314deef461290
结果:GCC和clang总是抱怨不完整类型的数组参数,而从不抱怨不完整类型的指针。
相关标准引用:

6.7.6.3函数声明符(包括原型)

[...]
4调整后,在函数声明符中作为该函数定义的一部分的参数类型列表中,参数不得具有不完整类型。
[...]
7将参数声明为“类型的数组”应调整为“类型的限定指针”,其中类型限定符(如果有)是在数组类型派生的[]内指定的。如果关键字static也出现在数组类型衍生的[]内,则对于每次调用该函数,相应实际参数的值应提供对至少具有与大小表达式指定的元素数量相同的元素的数组的第一个元素的访问。

嗯,到这里为止,似乎不完整类型的数组作为参数类型在定义中是完全可以接受的。

6.2.5类型

[...]
20可以从对象和函数类型构造任意数量的派生类型,如下所示:

  • 数组类型描述了具有特定成员对象类型(称为元素类型)的一组连续分配的非空对象。每当指定数组类型时,元素类型必须是完整的。数组类型以其元素类型和数组中的元素数目为特征。数组类型被称为从其元素类型派生的类型,如果其元素类型为T,则有时将数组类型称为“T的数组”。从元素类型构造数组类型称为“数组类型派生”。
以上引用明确禁止在任何情况下使用不完整类型的数组语法。
结论:所有这些编译器似乎都是正确的,尽管该限制似乎是不必要的。
无论如何,正确的做法不是在头文件中放置类型的前向声明,而是放置类型本身的声明,除非它应该是一个不透明类型。
在这种情况下,您将需要直接使用指针语法来表示参数类型。

1
作为提供另一种方法来实现OP想要的但假设他需要数据隐藏的辅助答案,我介绍这段代码,它建立在我的第一个答案之上,在一个C文件中为元素类型提供通用访问,并在头文件中仅提供不透明的数据类型。请注意,为了说明指针是什么,我使用了element*,但它们都可以被我在头文件中定义的ELEM_HANDLE替换。ELEM_HANDLE抽象了我们正在处理元素指针的事实。由于我们使用了不透明类型,我们提供了可以调用的方法(在element.h中定义)来处理我们的不透明类型。

element.h:

#include <stdint.h>

typedef struct element element;
typedef element *ELEM_HANDLE;

extern element *element_new();
extern void element_delete(element *elem);
extern void element_set_value_raw(element *elem, uint16_t value_raw);
extern uint16_t element_get_value_raw(element *elem);
extern void element_set_value_scaled(element *elem, float value_scaled);
extern float element_get_value_scaled(element *elem);
extern void element_set_desc(element *elem, char *desc);
extern char *element_get_desc(element *elem);

element.c:

#include <stdint.h>
#include <stdlib.h>

typedef struct element
{
        uint16_t value_raw;
        float value_scaled;
        char *desc;
} element;

element *element_new()
{
        return calloc(1, sizeof(element));
}

void element_delete(element *elem)
{
        free(elem);
}

void element_set_value_raw(element *elem, uint16_t value_raw)
{
        elem->value_raw = value_raw;
}
uint16_t element_get_value_raw(element *elem)
{
        return elem->value_raw;
}

void element_set_value_scaled(element *elem, float value_scaled)
{
        elem->value_scaled = value_scaled;
}

float element_get_value_scaled(element *elem)
{
        return elem->value_scaled;
}

void element_set_desc(element *elem, char *desc)
{
        elem->desc = desc;
}

char *element_get_desc(element *elem)
{
        return elem->desc;
}

testelem.c:

#include <stdio.h>
#include "element.h"

#define REG_READ_COUNT 2

void dostuff(element *sv[], int arrLen)
{
        int index;
        element *curelem;
        uint16_t raw;
        float scaled;
        char *desc;

        for (index = 0; index < arrLen ; index++){
                curelem = sv[index];
                raw = element_get_value_raw(curelem);
                scaled = element_get_value_scaled(curelem);
                desc = element_get_desc(curelem);
                /* Do more interesting stuff here */
                printf("%s, %d, %.4f\n", desc, raw, scaled);
        }
}

int main()
{
        unsigned int index;
        element *sv[REG_READ_COUNT]; /* array of element pointers*/
        char desc1[] = "The answer to everything";
        char desc2[] = "OtherStuff";

        /* Initialize an array of pointers to element items */
        for (index = 0; index < sizeof(sv) / sizeof(element *); index++)
                sv[index] = element_new();

        element_set_value_raw(sv[0], 42);
        element_set_value_scaled(sv[0], 6.66f);
        element_set_desc(sv[0], desc1);
        element_set_value_raw(sv[1], 123);
        element_set_value_scaled(sv[1], 456.7f);
        element_set_desc(sv[1], desc2);

        dostuff(sv, REG_READ_COUNT);

        /* free the array of pointers to element items*/
        for (index = 0; index < sizeof(sv) / sizeof(element *); index++)
                element_delete(sv[index]);

        return 0;
}

请注意,我在传递元素指针数组时,对 dostuff 的数组长度进行了修改。这为 dostuff 提供了足够的信息来确定数组中有多少个元素。这应该可以在C89或以上版本和C++编译器上正确编译(和运行),只需将 .c 文件重命名为 .cpp

我提供这个答案是因为使用前向声明和不透明类型是许多“C”语言共享对象创建的方式。这种机制允许将元素源编译成独立库或共享对象,并在不知道 element 数据类型长什么样的情况下使用它。实质上,我们在使用我们的模块和库之间提供了一个接口合同。如果我们修改 element.cpp 中结构元素的内部,则使用它的模块不需要重新编译(只需重新链接)。如果我们修改接口(合同),则使用库的客户端代码需要重新构建。

因此,最终可以使用前向引用(不透明类型)隐藏 C 数据类型的内部并提供一层抽象。这种机制通常由共享对象(.so 文件)用于构建可以被 C 程序使用的复杂库。


0
因为A.h只定义了一个不透明类型typedef struct element element,B.c无法知道element的组成甚至无法确定其大小。所以它无法创建这些结构体的数组。如果你想让这段代码工作,你需要将整个typedef移到A.c到A.h中。如果这样做,就没有信息隐藏,整个结构体可以通过头文件访问。
此外,你可以创建一个指向结构体的指针数组(即使它可能是不完整的),并将其传递给函数,但你无法直接访问任何结构体成员变量。
使用不透明数据类型在指向这些类型的指针数组中的示例:
typedef struct element element;
#define REG_READ_COUNT 100

void dostuff (element *sv[])
{
    sv++; /* get next pointer to element */
};

int main()
{
    element *sv[REG_READ_COUNT]; /* array of pointers to element */
    dostuff(sv);
}

这段代码很好,直到它需要使用实际类型的大小。我们甚至不能初始化数据成员为任何值,除非有额外的胶水代码(另一个模块)实际上可以访问完整的元素类型。

您可以拥有指针数组(甚至是不完整类型的指针数组)的原因是指针是C语言中的基本类型。它既不是不完整类型也不是函数类型。指针具有固定的大小,编译器可以用来生成指针数组。

6.7.5.2 数组声明符

约束条件

除了可选的类型限定符和关键字static之外,[和]可以限定表达式或*。如果它们限定一个表达式(指定数组的大小),则该表达式应具有整数类型。如果表达式是常量表达式,则其值应大于零。元素类型不得是不完整或函数类型。可选的类型限定符和关键字static仅出现在具有数组类型的函数参数的声明中,并且仅出现在最外层的数组类型派生中。

因为指针不是不完整类型或函数类型,即使它们指向不完整类型,您也可以创建它们的数组。指向不完整类型的指针并不使指针变得不完整。您只是不能直接引用它并希望对其进行任何有用的操作。我说直接是因为在数据隐藏技术和不透明指针中,您可以提供间接机制来处理不透明指针的数据。

这是一个代码示例,类似于 OP 的代码,应该无法编译通过。我们采用指向不完整类型的指针可以传递(函数参数)的想法,但它们仍然不能在函数内部用作数组:

typedef struct element element;
#define REG_READ_COUNT 100

void dostuff (element *sv) /* Completely legal but useless if you intend to use it as an array */
{
    sv++; /* This should fail - as we are doing array arithmetic on
           * an incomplete type. Can't find the starting point of the next
           * array element without knowing the size of the object */
};

int main()
{
    element sv[REG_READ_COUNT]; /* array of elements will also fail - size of object unknown */
    dostuff(sv);
}

这个和前面的几乎一样。在这个函数中,我们有一个指向不完整类型的指针sv作为参数(这是来自nneonneo的答案)。这是完全合法的,因为它只是一个指针。然而,在尝试对它进行数组算术运算时(在body函数中使用++),会失败,因为它需要知道元素的大小,但它不知道。 ++和--或索引数组是未定义行为(大多数符合标准的编译器将会报错)。 ISO/IEC 9899: TC2说:

6.3.2 其他操作数

6.3.2.1 左值、数组和函数设计者

...

2 除非它是 sizeof 运算符、一元 & 运算符、++ 运算符、-- 运算符或 . 运算符的左操作数或赋值运算符的左操作数,否则没有数组类型的左值将被转换为存储在指定对象中的值(不再是左值)。如果左值具有限定类型,则该值具有左值的无限定版本;否则,该值具有左值的类型。 如果左值具有不完整类型且没有数组类型,则行为未定义

关于不透明类型的更多信息可以在 这里 找到。


B.c 中,除了整个数组之外,是否定义了任何类型为 element 的元素? - Deduplicator
不是元素的数组,而是指向元素的指针数组。这是因为指针的大小在编译时已知。 - Michael Petch
请提供一个标准的引用来证明这一点。编译器错误是常有的事情... - Deduplicator
我会查找标准的参考资料。但是你应该注意,我没有像nneonneo建议的那样定义dostuff。他的函数原型和我的有所不同。然而,这段代码之所以能够工作,是因为我的代码在任何时候都不需要其他东西,只需要指向某些东西的指针。指针具有固定的大小,因此完全合法。 - Michael Petch
然后证明额外的间接引用是必要的,以使其合法,并且编译器不违反标准。 - Deduplicator

0

您的编译错误由Deduplicator的答案描述。

您可以通过编写element *sv来解决此问题。但是,B.c只能看到定义typedef struct element element;,它无法看到element的组成部分。

如果“真实”版本的dostuffsv执行任何需要知道该结构实际包含什么的操作,则需要将struct element的定义从A.c移动到A.h


注释是:“这需要知道该结构实际包含的内容”。这是正确的,但它还涉及到任何需要知道对象“大小”的操作。如果元素是一个完整的类型,那么参数element *sv允许我们这样做:sv++以获取下一个数组元素。如果element不完整的,并且参数是element *sv,那么当程序员执行sv++时,将会收到一个错误提示。这是因为数组算术需要知道大小(即使它不关心组成一个element的内容)。 - Michael Petch
@MichaelPetch,不知道内容就无法确定大小。数组计算需要内容可见。如果您不同意,请发布一个程序,在其中执行数组计算,但内容不可见。 - M.M
你所忽略的是size是由内容决定的。我们都同意这一点。对于不完整的类型,大小是未知的。然而,从编译器的角度来看,在进行数组算术运算时,它不再查看结构体的内部。它仅根据大小做出决策。它可以是100个字符或25个4字节的整数。数组算术运算不再关心实际内容。这就是我的观点,也是我要补充的唯一澄清。 - Michael Petch
我是指“它可以是一个100个字符或25个4字节整数的结构体”。只要所有内容加起来为100(在该示例中)。我可以在100字节(但不同内容)的对象之间进行类型转换,对两者进行指针算术运算,结果将相同。这只关乎我在尝试访问元素时那些数据结构中的内容。然后我最好处理正确的类型。 - Michael Petch
我不知道你想表达什么。在我的帖子中,我说“任何需要知道结构体实际包含内容的东西”。我们都同意指针算术需要知道结构体大小,而知道结构体大小需要知道内容。你似乎在说“编译器在计算出大小后可能会忘记内容”。如果是这样...那又怎样?在进行指针算术的代码编译过程中,它需要知道结构体包含的内容。 - M.M
显示剩余10条评论

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