我正在编写一个C库,它使用一些简单的面向对象的继承:
struct Base {
int x;
};
struct Derived {
struct Base base;
int y;
};
现在我想将Derived *传递给一个像这样的Base *的函数:
int getx(struct Base *arg) {
return arg->x;
};
int main() {
struct Derived d;
return getx(&d);
};
这很有用,当然是类型安全的,但是编译器不知道这一点。有没有办法告诉编译器这是类型安全的?我只关注GCC并在这里铿锵作响,因此欢迎编译器特定的答案。我有一些模糊的记忆,看到一些代码使用__attribute__((inherits(Base))
或类似的东西来做这件事,但我的记忆可能在撒谎。
答案 0 :(得分:5)
这在C中是安全的,除非你应该将参数强制转换为Base *
。禁止别名的规则(或更确切地说,不包括在标准C中支持的规则)在C 2011 6.5中,其中第7段规定:
对象的存储值只能由具有以下类型之一的左值表达式访问:
- 与对象的有效类型兼容的类型,
...
此规则阻止我们指向float
,将其转换为指向int
的指针,并取消引用指向int
的指针以float
作为{ {1}}。 (更准确地说,它并不妨碍我们尝试,但它会使行为不明确。)
您的代码似乎违反了此规范,因为它使用int
左值访问Derived
对象。但是,C 2011 6.7.2.1第15段规定支持将指针Base
转换为指向Derived
的指针:
...指向适当转换的结构对象的指针指向其初始成员...
因此,当我们将指向Base
的指针转换为指向Derived
的指针时,我们实际拥有的不是指向使用与其不同类型的Base
对象的指针(这是一个指向Derived
对象的第一个成员的指针,使用它的实际类型Derived
,这是完全正常的。
关于编辑:最初我声明函数参数将被转换为参数类型。但是,C 6.5.2.2 2要求每个参数都具有可以分配给具有相应参数类型的对象的类型(删除Base
等任何限定条件),6.5.16.1要求在分配时一个指向另一个的指针,它们具有兼容的类型(或满足其他不适用的条件)。因此,将指向const
的指针传递给指向Derived
的函数会违反标准C约束。但是,如果您自己执行转换,则它是合法的。如果需要,转换可以内置到调用函数的预处理器宏中,这样代码看起来仍然像一个简单的函数调用。
答案 1 :(得分:1)
提供基本成员的地址(真正的类型安全选项):
getx(&d.base);
或者使用void指针:
int getx(void * arg) {
struct Base * temp = arg;
return temp->x;
};
int main() {
struct Derived d;
return getx(&d);
};
它有效,因为C要求在第一个struct成员之前永远不会有填充。这不会增加类型安全性,但会消除对铸造的需求。
答案 2 :(得分:1)
如上所述,用户694733,您可能最好通过使用基本字段的地址来符合标准和类型安全性(重复以供将来参考)
struct Base{
int x;
}
struct Derived{
int y;
struct Base b; /* look mam, not the first field! */
}
struct Derived d = {0}, *pd = &d;
void getx (struct Base* b);
现在尽管基地不是你仍然可以做的第一个领域
getx (&d.b);
或者如果你正在处理指针
getx(&pd->b).
这是一个非常常见的习语。但是,如果指针为NULL,则必须小心,因为& pd-> b只是
(struct Base*)((char*)pd + offsetof(struct Derived, b))
so&((Derived *)NULL) - > b变为
((struct Base*)offsetof(struct Derived, b)) != NULL.
IMO错失的机会是C采用匿名结构但没有采用计划9匿名结构模型
struct Derived{
int y;
struct Base; /* look mam, no fieldname */
} d;
它允许你只写getx(& d),编译器会将Derived指针调整为基指针,即它与上面例子中的getx(& d.b)完全相同。换句话说,它有效地为您提供继承,但具有非常具体的内存布局模型。特别是,如果你坚持不在顶部嵌入(==继承)基础结构,你必须自己处理NULL。正如您对继承所期望的那样,它对于
是递归的struct TwiceDerived{
struct Derived;
int z;
} td;
你仍然可以写getx(& td)。此外,您可能不需要getx,因为您可以编写d.x(或td.x或pd-> x)。
最后使用typeof gcc扩展,你可以编写一个用于向下转换的小宏(即转换为更加派生的结构)
#define TO(T,p) \
({ \
typeof(p) nil = (T*)0; \
(T*)((char*)p - ((char*)nil - (char*)0)); \
}) \
所以你可以做像
这样的事情struct Base b = {0}, *pb = &b;
struct Derived* pd = TO(struct Derived, pb);
如果您尝试使用函数指针执行虚函数,这将非常有用。
在gcc上,您可以使用/试验计划9扩展名-fplan9-extensions。不幸的是,它似乎没有在clang上实现。