您能否给出三个 C 标准(最好是 C99 或 C11)之一的章节和经文,指出以下头文件中是否包含一种或两种struct uperms_entry
类型?
#ifndef UPERMS_CACHE_INCLUDE
#define UPERMS_CACHE_INCLUDE
typedef struct mutex MT_MUTEX;
typedef struct uperms_cache
{
MT_MUTEX *cache_lock;
int processing;
struct uperms_entry *uperms_list; // No prior struct uperms_entry
} uperms_cache_t;
typedef struct uperms_entry // Does this define a different struct uperms_entry?
{
char username[32];
int perms;
struct uperms_entry *next;
} uperms_entry_t;
#endif /* UPERMS_CACHE_INCLUDE */
附加问题:
- 如果有两种类型,有没有办法让 GCC 报告问题?
- 如果有两种类型,在实践中是否重要?
(我认为答案是“是的——严格来说有两种类型”,然后是(1)否和(2)否。)
背景:内部代码审查——我想颠倒结构的顺序,但我不确定我是否完全过于迂腐。
更新:
显然,最初问题的答案是“有一个struct uperms_entry
”,因此编号为 1 和 2 的问题没有实际意义。我很高兴在进行代码审查之前进行了检查。
背景思维
这部分是在主要问题解决很久之后添加的。
以下是来自 ISO/IEC 9899:2011 的一些广泛但相关的引述:
§6.2.7 兼容类型和复合类型
¶1 如果它们的类型相同,则两种类型具有兼容的类型。用于确定两种类型是否兼容的附加规则在 6.7.2 中描述了类型说明符,在 6.7.3 中描述了类型限定符,在 6.7.6 中描述了声明符。55)此外,如果它们的标记和成员满足以下要求,则在单独的翻译单元中声明的两种结构、联合或枚举类型是兼容的:如果用标记声明了一个,则应用相同的标记声明另一个。如果两者都在各自翻译单元内的任何地方完成,则适用以下附加要求:它们的成员之间应存在一一对应关系,以便每对对应的成员都声明为兼容的类型;如果该对的一个成员使用对齐说明符声明,则另一个成员使用等效的对齐说明符声明;如果该对中的一个成员声明了一个名称,则另一个成员声明为相同的名称。对于两个结构,相应的成员应以相同的顺序声明。对于两个结构或联合,相应的位域应具有相同的宽度。对于两个枚举,对应的成员应具有相同的值。
55)两种类型不必相同才能兼容。
§6.7.2.1 结构和联合说明符
¶8 struct-or-union-specifier 中 struct-declaration-list 的存在在翻译单元内声明了一个新类型。struct-declaration-list 是结构或联合成员的一系列声明。如果 struct-declaration-list 直接或通过匿名结构或匿名联合不包含任何命名成员,则行为未定义。直到终止列表之后,类型才不完整
}
,然后完成。§6.7.2.3 标签
¶4 具有相同范围并使用相同标记的结构、联合或枚举类型的所有声明都声明了相同的类型。不管在同一翻译单元中是否存在标记或该类型的其他声明,该类型是不完整的129)直到紧接在定义内容的列表的右大括号之后,然后才完成。
¶5 在不同范围内或使用不同标签的结构、联合或枚举类型的两个声明声明了不同的类型。不包含标记的结构、联合或枚举类型的每个声明都声明了不同的类型。
¶6 形式的类型说明符
struct-or-union identifier
选择{ struct-declaration-list }
或者
enum identifier
选择{ enumerator-list }
或者
enum identifier
选择{ enumerator-list , }
声明结构、联合或枚举类型。列表定义结构内容、联合内容或枚举内容。如果提供了标识符,则130)类型说明符还将标识符声明为该类型的标记。
¶7 表格声明
struct-or-union identifier ;
指定结构或联合类型并将标识符声明为该类型的标记。131)
¶8如果形式的类型说明符
struct-or-union identifier
除了作为上述形式之一的一部分出现,并且没有其他标识符作为标记的声明是可见的,那么它声明了一个不完整的结构或联合类型,并将标识符声明为该类型的标记。131)
¶9如果形式的类型说明符
struct-or-union identifier
或者
enum identifier
出现不是作为上述形式之一的一部分,并且标识符作为标记的声明是可见的,则它指定与其他声明相同的类型,并且不重新声明该标记。
¶12 示例 2 为了说明使用标记的先前声明来指定一对相互引用的结构,声明
struct s1 { struct s2 *s2p; /* ... */ }; // D1 struct s2 { struct s1 *s1p; /* ... */ }; // D2
指定一对包含彼此指针的结构。但是请注意,如果 s2 已被声明为封闭范围内的标记,则声明 D1 将引用它,而不是 D2 中声明的标记 s2。为了消除这种上下文敏感性,声明
struct s2;
可以在 D1 之前插入。这在内部范围内声明了一个新标签 s2;然后,声明 D2 完成了新类型的规范。
129)不完整类型只能在不需要该类型对象的大小时使用。例如,当 typedef 名称被声明为结构或联合的说明符时,或者在声明指向或返回结构或联合的函数时,不需要它。(参见 6.2.5 中的不完整类型。)在调用或定义这样的函数之前,规范必须是完整的。
130)如果没有标识符,则该类型在翻译单元内只能由它所属的声明引用。当然,当声明是 typedef 名称时,后续声明可以使用该 typedef 名称来声明具有指定结构、联合或枚举类型的对象。
131)不存在与枚举类似的构造。
§6.7.3 类型限定符
¶10 对于要兼容的两个合格类型,两者都应具有兼容类型的相同合格版本;说明符或限定符列表中类型限定符的顺序不影响指定的类型。
§6.7.6 中的讨论与指针、数组和函数声明符有关,并不真正影响结构或联合。
当我写这个问题时,我知道示例 2。这是一些关于上述信息意味着什么的大声思考。
考虑这个编译干净的例子:
#include <stdio.h>
struct r1 { int x; };
struct r1;
struct r1 p0;
//struct r1 { int y; }; // Redefinition of struct r1
extern void z(void);
void z(void)
{
struct r1 p1 = { 23 };
struct r1;
//struct r1 p2; // Storage size of p2 is not known
struct r2 { struct r1 *rn; int y; };
struct r1 { struct r2 *rn; int z; };
struct r2 p = { 0, 1 };
struct r1 q = { &p, 2 };
p.rn = &q;
printf("p.y = %d, q.z = %d\n", p.y, q.z);
printf("p1.x = %d\n", p1.x);
}
该函数说明了示例 2 何时适用,但不是明智的代码。函数中的声明p1
是与全局变量相同类型的结构p0
。尽管它的类型名称是struct r1
,但它与局部变量的类型是不同的(且不兼容的)类型p
。
struct r1
无论元素是命名x
还是,都不允许在全局级别重新定义y
。在这种情况下,先验
struct r1;
是无操作的。
一个有趣的问题是'函数可以z
传递p
或传递q
给任何其他函数(调用它a
)吗?答案是合格的“是”,其中一些限制很有趣。(尝试它也是令人震惊的编码风格,近乎疯狂。)该功能必须存在于单独的翻译单元(TU)中。函数声明必须在函数内部z
(因为如果在函数外部,它的原型必须引用struct r1
函数外部定义的,而不是struct
r1
内部定义的。
在另一个 TU 中,必须有一定程度的理智:函数a
必须具有兼容的结构类型struct r1
并且struct r2
在其全局范围内可见。
这是另一个例子,但这个没有编译:
#include <stdio.h>
struct r1;
extern void z(struct r1 *r1p);
extern void y(struct r1 *r1p);
void y(struct r1 *r1p)
{
struct r2 { struct r1 *rn; int y; };
struct r1 { struct r2 *rn; int z; };
struct r2 p = { r1p, 1 };
struct r1 q = { &p, 2 };
p.rn = &q;
printf("p.y = %d, q.z = %d\n", p.y, q.z);
}
void z(struct r1 *r1p)
{
struct r1
struct r2 { struct r1 *rn; int y; };
struct r1 { struct r2 *rn; int z; };
struct r2 p = { r1p, 1 };
struct r1 q = { &p, 2 };
p.rn = &q;
printf("p.y = %d, q.z = %d\n", p.y, q.z);
}
来自 Mac OS X 10.7.4 上的 GCC 4.7.1 的警告是:
structs3.c: In function 'y':
structs3.c:13:10: warning: assignment from incompatible pointer type [enabled by default]
structs3.c: In function 'z':
structs3.c:22:12: warning: initialization from incompatible pointer type [enabled by default]
structs3.c:22:12: warning: (near initialization for 'p.rn') [enabled by default]
p.rn = &q;
第 13 行是函数中的赋值,第 23 行是尝试在函数y
中定义和初始化。struct r2 p
z
这表明在函数中,rn
元素 ofstruct
r2
是指向struct r1
在全局范围内声明的不完整类型的指针。在函数内添加 astruct r1;
作为第一行代码将允许代码编译,但初始化引用r1p->rn
再次取消引用指向不完整类型的指针(不完整类型是struct r1
在全局范围内声明的)。
函数声明和前struct r1;
一行可以作为不透明类型出现在标题中。支持功能列表不完整;需要一种方法来获取指向初始化的指针以struct r1
传递给函数,但这是一个细节。
为了使代码在第二个 TU 中工作,在struct r1
定义函数之前,for 的类型必须在全局范围内是完整的,并且由于递归引用,`struct r21 也必须是完整的。
#include <stdio.h>
/* Logically in a 3-line header file */
struct r1;
extern void z(struct r1 *r1p);
extern void y(struct r1 *r1p);
/* Details private to this TU */
struct r2 { struct r1 *rn; int y; };
struct r1 { struct r2 *rn; int z; };
void y(struct r1 *r1p)
{
struct r2 p = { r1p, 1 };
struct r1 q = { r1p->rn, 2 };
p.rn = &q;
printf("p.y = %d, q.z = %d\n", p.y, q.z);
}
void z(struct r1 *r1p)
{
struct r2 p = { r1p, 1 };
struct r1 q = { r1p->rn, 2 };
p.rn = &q;
printf("p.y = %d, q.z = %d\n", p.y, q.z);
}
如果需要,可以在多个实现文件中重复在实现文件中定义结构而在公共头文件中保留类型不完整的过程,但如果多个 TU 使用完整的结构定义,则最好放置定义在仅在实现结构的文件之间共享的私有头文件中。我注意到私有标头在公共标头之前还是之后都没有关系。
也许这对你来说已经很明显了。以前我不需要在这个细节层次上仔细考虑。