在C中,堆栈和堆创build“类”?
每当我看到一个C“类”(任何结构的意图是通过访问函数,将指针作为第一个参数使用),我看到他们这样实现:
typedef struct { int member_a; float member_b; } CClass; CClass* CClass_create(); void CClass_destroy(CClass *self); void CClass_someFunction(CClass *self, ...); ...
在这种情况下, CClass_create
总是malloc
它的内存并返回一个指针。
每当我看到new
C ++不必要的时候,它通常会使C ++程序员疯狂,但这种做法在C中似乎是可以接受的。 为什么堆分配结构“类”是如此普遍背后有什么理由?
有几个原因。
- 使用“不透明”的指针
- 缺乏析构函数
- embedded式系统(堆栈溢出问题)
- 集装箱
- 惯性
- “懒惰”
让我们简单地讨论一下。
对于不透明的指针 ,它可以让你做一些事情:
struct CClass_; typedef struct CClass_ CClass; // the rest as in your example
所以,用户没有看到struct CClass_
的定义,将她从变化中隔离出来,并且启用了其他有趣的东西,比如在不同的平台上实现不同的类。
当然,这禁止使用CClass
堆栈variables。 但是,OTOH可以看出,这并不禁止静态(从某个池)分配CClass
对象 – 由CClass_create
返回,或者可能是另一个函数,如CClass_create_static
。
缺乏析构函数 – 因为C编译器不会自动破坏你的CClass
堆栈对象,所以你需要自己去做(手动调用析构函数)。 所以,剩下的唯一好处是堆栈分配通常比堆分配快。 OTOH,你不必使用堆 – 你可以从一个池,或一个竞技场,或者这样的东西分配,这可能几乎与堆栈分配一样快,没有下面讨论的堆栈分配的潜在问题。
embedded式系统 – 堆栈不是一个“无限”的资源,你知道的。 当然,对于今天的“常规”操作系统(POSIX,Windows …)上的大多数应用程序,它几乎是。 但是,在embedded式系统上,堆栈可能只有几个KB。 这是极端的,但即使是“大”的embedded式系统也有以MB为单位的堆栈。 所以,如果过度使用,它将用完。 当它的时候,大部分都不能保证会发生什么 – AFAIK,C和C ++都是“未定义的行为”。 OTOH,当内存不足时, CClass_create()
可以返回NULL指针,并且可以处理该指针。
容器 – C ++用户喜欢堆栈分配,但是,如果您在堆栈上创buildstd::vector
,则其内容将被堆分配。 当然,你可以调整它,但这是默认的行为,而且让一个容器的所有成员都是堆分配,而不是试图弄清楚如何处理。
惯性 – 呃,OO来自SmallTalk。 在那里一切都是dynamic的,所以,对C的“自然”翻译就是“把所有东西都堆在一起”的方式。 所以,第一个例子就是这样,他们激励了很多年。
“ 懒惰 ” – 如果你知道你只想要堆栈对象,你需要这样的东西:
CClass CClass_make(); void CClass_deinit(CClass *me);
但是,如果你想允许堆栈和堆,你需要添加:
CClass *CClass_create(); void CClass_destroy(CClass *me);
对于实现者来说这是更多的工作,但是也会让用户感到困惑。 人们可以做出稍微不同的接口,但是这并不改变你需要两套function的事实。
当然,“容器”的原因也是部分“懒惰”的原因。
假设,如你的问题, CClass_create
和CClass_destroy
使用malloc/free
,那么对我来说,以下是不好的做法:
void Myfunc() { CClass* myinstance = CClass_create(); ... CClass_destroy(myinstance); }
因为我们可以轻松地避免malloc和free:
void Myfunc() { CClass myinstance; // no malloc needed here, myinstance is on the stack CClass_Initialize(&myinstance); ... CClass_Uninitialize(&myinstance); // no free needed here because myinstance is on the stack }
同
CClass* CClass_create() { CClass *self= malloc(sizeof(CClass)); CClass_Initialize(self); return self; } void CClass_destroy(CClass *self); { CClass_Uninitialize(self); free(self); } void CClass_Initialize(CClass *self) { // initialize stuff ... } void CClass_Uninitialize(CClass *self); { // uninitialize stuff ... }
在C ++中,我们也宁愿这样做:
void Myfunc() { CClass myinstance; ... }
比这个:
void Myfunc() { CClass* myinstance = new CCLass; ... delete myinstance; }
为了避免不必要的new
/ delete
。
在C中,当某个组件提供“创build”function时,组件实现者也控制组件的初始化。 所以它不仅模仿 C ++的operator new
而且也模拟了类的构造函数。
放弃对初始化的这种控制意味着对input进行更多的错误检查,因此保持控制可以更容易地提供一致和可预测的行为。
我也对malloc
总是用来分配内存。 这可能经常是这种情况,但并不总是如此。 例如,在一些embedded式系统中,你会发现malloc
/ free
根本不被使用。 X_create
函数可以以其他方式分配,例如,从编译时固定大小的数组中分配。
这产生了很多答案,因为它有点基于意见 。 我仍然想解释为什么我个人更喜欢在堆上分配“C对象”。 原因是我的领域都隐藏(说: 私人 )从消费代码。 这被称为不透明的指针 。 在实践中,这意味着你的头文件没有定义使用的struct
,它只是声明它。 直接的结果是,消费代码无法知道struct
的大小,因此堆栈分配变得不可能。
好处是:消耗代码永远不会依赖于struct
的定义,这意味着不可能以某种方式从外部呈现struct
的内容不一致, 并且避免在struct
更改时不必要地重新编译消耗代码。
第一个问题是在c ++中声明域是private
。 但是class
的定义仍然在所有使用它的编译单元中导入,即使只有您的private
成员发生更改,也需要重新编译它们。 在c ++中经常使用的解决scheme是pimpl
模式:将所有私有成员放在第二个struct
(或: class
)中,这个成员只在实现文件中定义。 当然,这需要你的pimpl
被分配在堆上。
除此之外, 现代的 OOP语言(例如java或者c# )有了分配对象的方法(通常决定内部是堆栈还是堆栈),而不需要调用代码知道它们的定义。
一般来说,你看到*
事实并不意味着它已经被malloc
。 例如,你可能会得到一个指向static
全局variables的指针。 在你的情况下,事实上, CClass_destroy()
不会带任何参数,它假设它已经知道有关被销毁的对象的一些信息。
而且,无论是否使用malloc
,指针都是唯一允许修改对象的方法。
我没有看到使用堆而不是堆栈的特殊原因:你没有得到更less的内存使用。 但是,初始化这些“类”需要的是init / destroy函数,因为底层的数据结构可能实际上需要包含dynamic数据,因此需要使用指针。
我会将“构造函数”更改为void CClass_create(CClass*);
它不会返回结构体的实例/引用,而是被调用。
至于它是在“堆栈”还是dynamic分配,完全取决于您的使用场景要求。 然而,你分配它,你只需调用CClass_create()
传递分配的结构作为参数。
{ CClass stk; CClass_create(&stk); CClass *dyn = malloc(sizeof(CClass)); CClass_create(dyn); CClass_destroy(&stk); // the local object lifetime ends here, dyn lives on } // and later, assuming you kept track of dyn CClass_destroy(dyn); // destructed free(dyn); // deleted
只要小心不要返回一个本地(分配在堆栈上)的引用,因为这是UB。
但是你分配它,你需要调用void CClass_destroy(CClass*);
在正确的位置(该对象的生命周期结束),如果dynamic分配,也释放该内存。
区分分配/取消分配和构造/销毁, 这些是不一样的 (即使在C ++中它们可能会自动耦合在一起)。
因为如果函数不包含指向其他分配的结构体的指针,函数只能返回一个堆栈分配的结构体。 如果它只包含简单对象(int,bool,float,chars和它们的数组,但没有指针 ),则可以将它分配给堆栈。 但是你必须知道,如果你回报它,它将被复制。 如果你想允许指向其他结构的指针,或者想要避免复制然后使用堆。
但是,如果你可以在顶层的单元中创build结构,并且只在被调用的函数中使用它,并且永远不会返回它,那么栈就是合适的
如果需要同时存在的某种types的最大对象数量是固定的,那么系统将需要能够对每个“活”实例做些事情,并且所讨论的项目不会消耗太多的钱,最好的方法通常既不是堆分配也不是堆栈分配,而是一个静态分配的数组,以及“创build”和“销毁”方法。 使用数组可以避免维护一个链接的对象列表,并且可以处理一个对象因为“繁忙”而不能被立即销毁的情况[例如,如果数据通过中断到达某个通道或DMA,当用户代码决定不再对该通道感兴趣并处理该通道时,用户代码可以设置“处理完成”标志并返回而不必担心具有未被分配的未决中断或DMA覆盖存储它]。
使用固定大小的固定大小的对象池使分配和取消分配比从混合大小的堆中获取存储更可预测。 这种方法在需求变化且对象占用大量空间(单独或集体)的情况下并不是很好,但当需求大部分是一致的时候(例如应用程序总是需要12个对象,有时需要3个更多)它可以比其他方法更好地工作。 一个缺点是任何设置都必须在声明静态缓冲区的位置执行,或者必须由客户端中的可执行代码执行。 客户端无法使用variables初始化语法。
顺便说一句,当使用这种方法时,不需要让客户端代码接收任何指针。 相反,可以使用任何大小的整数来方便地识别资源。 此外,如果资源数量永远不会超过int
的位数,那么每个资源有一些状态variables使用一位可能会有所帮助。 例如,可以具有variablestimer_notifications
(仅通过中断处理程序写入)和timer_acks
(仅通过主线代码写入),并指定定时器N需要服务时将设置(timer_notifications ^ timer_acks)
位N。 使用这种方法,代码只需要读取两个variables来确定是否有定时器需要服务,而不必为每个定时器读取一个variables。
C缺乏某些C ++程序员认为理所当然的事情。
- 公共和私人说明符
- 构造函数和析构函数
这种方法的一大优点是可以隐藏C文件中的结构,并使用create和destroy函数强制执行正确的构造和破坏。
如果您在.h文件中公开结构,这将意味着用户可以直接访问破坏封装的成员。 也不要强制创build允许不正确的构造你的对象。
你的问题是“为什么在C中dynamic分配内存是正常的,而在C ++中是不正常的”?
C ++有很多构造,使得新的冗余。 复制,移动和正常的构造函数,析构函数,标准库,分配器。
但在C中,你无法绕过它。
它实际上是一个反弹,使C ++使“新”太容易。
从理论上讲,在C中使用这个类的构造模式和在C ++中使用“new”是一样的,所以应该没有区别。 然而,人们倾向于思考语言的方式是不同的,所以人们对代码的反应方式是不同的。
在C中,考虑计算机为实现目标所需执行的确切操作是非常常见的。 这不是普遍的,但它是一个非常普遍的心态。 假设您已经花时间做了malloc / free的成本/收益分析。
在C ++中,编写代码对你来说非常有用,而你甚至没有意识到这一点。 有人写一行代码是很常见的,甚至没有意识到发生了100或200次新的/删除的情况! 这引起了一场反弹,C ++开发人员会因为新闻和删除而狂妄地挑选新闻,因为担心他们被意外地称为遍地。
当然,这些是概括性的。 整个C和C ++社区绝不适合这些模具。 但是,如果你正在使用新的而不是把堆放在堆上,这可能是根本原因。
你常常看到这很奇怪。 你一定是在看一些“懒惰”的代码。
在C中,所描述的技术通常被保留为“不透明”的库types,即结构types的定义有意使客户端的代码不可见。 由于客户端不能声明这样的对象,这个习惯用法在“隐藏”库代码中必须真正地进行dynamic分配。
当隐藏结构的定义不是必需的,一个典型的C语言通常看起来如下
typedef struct CClass { int member_a; float member_b; } CClass; CClass* CClass_init(CClass* cclass); void CClass_release(CClass* cclass);
函数CClass_init
初始化*cclass
对象,并返回与结果相同的指针。 即为对象分配内存的负担放在调用者上,调用者可以以任何它认为合适的方式分配
CClass cclass; CClass_init(&cclass); ... CClass_release(&cclass);
这个习惯用法的典型例子是pthread_mutex_t
和pthread_mutex_init
和pthread_mutex_destroy
。
同时,对于非透明types使用前一种技术(如您的原始代码)通常是一个值得怀疑的做法。 在C ++中无条件地使用dynamic内存的确是有问题的。 它的工作原理,但是,当不需要的时候使用dynamic内存就像在C ++中一样在C中皱起眉头。