信息隐藏
与在函数的return语句中返回整个结构相比,返回指向结构的指针有什么好处?
最常见的一种是信息隐藏。举例来说,C没有能力使字段成为struct
私有字段,更不用说提供访问它们的方法了。
因此,如果您要强制阻止开发人员查看和篡改pointee的内容(例如)FILE
,则唯一的方法是通过将指针视为不透明的指针(指向pointer的大小和大小)来防止其暴露于其定义。定义是外界所不知道的。这样,的定义FILE
将仅对那些实现需要其定义的操作(例如)的用户fopen
可见,而只有结构声明对公共头来说才可见。
二进制兼容性
隐藏结构定义还可以帮助提供喘息的空间,以保持dylib API中的二进制兼容性。它允许库实现者更改不透明结构中的字段,而不会破坏与使用库的人的二进制兼容性,因为其代码的本质只需要知道他们可以对结构进行什么操作,而无需知道结构的大小或字段的大小它有。
例如,我实际上可以运行一些在Windows 95时代构建的古老程序(并不总是完美的,但令人惊讶的是,许多程序仍然可以运行)。可能是那些古老的二进制文件的某些代码使用了不透明的指针指向其大小和内容从Windows 95时代开始发生变化的结构。但是,由于这些程序没有暴露于这些结构的内容中,因此它们仍可以在新版本的Windows中继续工作。当在二进制兼容性很重要的库上工作时,通常允许更改客户端不暴露的内容而不会破坏向后兼容性。
效率
我想返回一个完整的NULL结构会更困难,或者效率更低。这是正当的理由吗?
假设类型实际上可以适应并在堆栈上分配,通常效率较低,除非幕后使用的通用内存分配器通常malloc
比已分配的固定大小而不是可变大小的分配器池少得多。在这种情况下,最有可能的安全折衷方案是允许库开发人员维护与相关的不变式(概念保证)FILE
。
至少从性能的角度来看,fopen
返回指针并不是一个合理的理由,因为它返回的唯一原因NULL
是无法打开文件。这将优化一个例外情况,以换取放慢所有常见情况的执行路径。在某些情况下,可能存在有效的生产力原因,可以使设计更直接,使它们返回指针,以允许NULL
在某些后置条件下返回。
对于文件操作,与文件操作本身相比,开销相对来说是微不足道的,并且fclose
无论如何都无法避免手动操作。因此,这不像我们可以通过公开定义FILE
并按值返回资源来节省客户端释放(关闭)资源的麻烦,fopen
或者考虑到文件操作本身的相对成本以避免堆分配,期望性能会大大提高。
热点和修复
但是,对于其他情况,我在旧代码库中配置了很多浪费的C代码,这些代码中有热点,malloc
并且不必要的强制性高速缓存未命中,这是由于这种做法经常与不透明的指针一起使用,并且在堆上不必要地分配了太多东西,有时在大循环。
我使用的另一种做法是,通过使用命名约定标准来传达结构定义,即使客户端不打算篡改它们,也不要暴露任何其他字段:
struct Foo
{
/* priv_* indicates that you shouldn't tamper with these fields! */
int priv_internal_field;
int priv_other_one;
};
struct Foo foo_create(void);
void foo_destroy(struct Foo* foo);
void foo_something(struct Foo* foo);
如果将来存在二进制兼容性问题,那么我发现它足以为将来的目的多余地保留一些额外的空间,例如:
struct Foo
{
/* priv_* indicates that you shouldn't tamper with these fields! */
int priv_internal_field;
int priv_other_one;
/* reserved for possible future uses (emergency backup plan).
currently just set to null. */
void* priv_reserved;
};
保留的空间有点浪费,但是如果将来我们发现需要添加更多数据Foo
而不破坏使用我们库的二进制文件,那么可以节省生命。
在我看来,信息隐藏和二进制兼容性通常是仅允许除可变长度结构之外的其他堆结构分配的唯一正当理由(这将总是需要它,或者如果客户端不得不分配,至少会有点尴尬,否则使用它以VLA方式在堆栈上分配内存以分配VLS)。即使大型结构通常也更便宜以值返回,如果这意味着该软件可以更有效地使用堆栈中的热内存。即使以创造价值获得回报并不便宜,人们也可以这样做:
int foo_create(struct Foo* foo);
...
/* In the client code: */
struct Foo foo;
if (foo_create(&foo))
{
foo_something(&foo);
foo_destroy(&foo);
}
... Foo
从堆栈进行初始化,而不会产生多余的副本。或者,Foo
如果出于某种原因,客户端甚至可以自由地在堆上进行分配。