在C中创建"类",在堆栈上与堆相比?

The*_*ang 47 c heap stack struct

每当我看到一个C"类"(通过访问将指针作为第一个参数的函数来使用的任何结构)时,我看到它们实现如下:

typedef struct
{
    int member_a;
    float member_b;
} CClass;

CClass* CClass_create();
void CClass_destroy(CClass *self);
void CClass_someFunction(CClass *self, ...);
...
Run Code Online (Sandbox Code Playgroud)

在这种情况下,它CClass_create始终malloc是内存并返回指向它的指针.

每当我看到newC++不必要地出现时,它似乎通常会让C++程序员疯狂,但这种做法在C中似乎是可以接受的.为什么堆分配的结构"类"如此常见?

srd*_*vic 50

有几个原因.

  1. 使用"不透明"指针
  2. 缺乏析构函数
  3. 嵌入式系统(堆栈溢出问题)
  4. 集装箱
  5. 惯性
  6. "懒惰"

我们来简单讨论一下.

对于不透明指针,它使您可以执行以下操作:

struct CClass_;
typedef struct CClass_ CClass;
// the rest as in your example
Run Code Online (Sandbox Code Playgroud)

因此,用户没有看到它的定义struct CClass_,使她免受对它的更改并启用其他有趣的东西,比如为不同的平台实现不同的类.

当然,这禁止使用堆栈变量CClass.但是,OTOH,可以看出这并不禁止CClass静态地(从某个池中)分配对象 - 由CClass_create或者可能是另一个函数返回CClass_create_static.

缺少析构函数 - 由于C编译器不会自动破坏您的CClass堆栈对象,您需要自己完成(手动调用析构函数).因此,剩下的唯一好处是堆栈分配通常比堆分配更快.OTOH,您不必使用堆 - 您可以从池,竞技场或某些此类事物进行分配,这可能几乎与堆栈分配一样快,而不会出现下面讨论的堆栈分配的潜在问题.

嵌入式系统 - 堆栈不是"无限"资源,你知道.当然,对于今天的"常规"操作系统(POSIX,Windows ......)上的大多数应用程序,它几乎都是.但是,在嵌入式系统上,堆栈可能低至几KB.这是极端的,但即使是"大"的嵌入式系统也有以MB为单位的堆栈.因此,如果过度使用它将会耗尽.当它发生时,大多数情况下无法保证会发生什么--AFAIK,在C和C++中都是"未定义的行为".OTOH,CClass_create()当你内存不足时可以返回NULL指针,你可以处理它.

容器 - C++用户喜欢堆栈分配,但是,如果你创建一个std::vector堆栈,它的内容将被堆分配.当然,您可以调整一下,但这是默认行为,它使人们更容易说"容器的所有成员都是堆分配的",而不是试图弄清楚如何处理它们.

惯性 - 好吧,OO来自SmallTalk.那里的一切都是动态的,因此,对C的"自然"翻译是"把所有东西放在堆里"的方式.所以,第一个例子就是这样,他们多年来一直鼓舞他人.

" 懒惰 " - 如果你知道你只想要堆栈对象,你需要这样的东西:

CClass CClass_make();
void CClass_deinit(CClass *me);
Run Code Online (Sandbox Code Playgroud)

但是,如果要同时允许堆栈和堆,则需要添加:

CClass *CClass_create();
void CClass_destroy(CClass *me);
Run Code Online (Sandbox Code Playgroud)

这对于实现者来说是更多的工作,但也使用户感到困惑.可以使接口略有不同,但它不会改变您需要两组功能的事实.

当然,"容器"的原因也部分是"懒惰"的原因.

  • +1为完整性,在这里看不到任何遗漏.有关*opaque指针*好处的详细说明,请参阅我的回答. (3认同)

Jab*_*cky 14

假设,如你的问题CClass_createCClass_destroy使用malloc/free,那么对我来说,做以下是不好的做法:

void Myfunc()
{
  CClass* myinstance = CClass_create();
  ...

  CClass_destroy(myinstance);
}
Run Code Online (Sandbox Code Playgroud)

因为我们可以轻松地避免使用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
}
Run Code Online (Sandbox Code Playgroud)

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
   ...
}
Run Code Online (Sandbox Code Playgroud)

在C++中我们也宁愿这样做:

void Myfunc()
{
  CClass myinstance;
  ...

}
Run Code Online (Sandbox Code Playgroud)

比这个:

void Myfunc()
{
  CClass* myinstance = new CCLass;
  ...

  delete myinstance;
}
Run Code Online (Sandbox Code Playgroud)

为了避免不必要的new/ delete.


小智 9

在C中,当某个组件提供"创建"功能时,组件实现者也可以控制组件的初始化方式.所以它不仅模仿 C++' operator new而且模仿类构造函数.

放弃对初始化的这种控制意味着对输入进行更多错误检查,因此保持控制可以更容易地提供一致且可预测的行为.

我也malloc 常常被用来分配内存.通常情况可能如此,但并非总是如此.例如,在某些嵌入式系统中,您会发现malloc/ free根本不使用/ .这些X_create函数可以以其他方式分配,例如,从编译时大小固定的数组中分配.


小智 8

这产生了很多答案,因为它有点基于意见.我仍然想解释为什么我个人更喜欢在堆上分配我的"C对象".原因是我的字段全部隐藏(说话:私有)消费代码.这称为不透明指针.实际上,这意味着您的头文件没有定义struct正在使用中,它只声明它.直接后果是,消耗代码无法知道其大小,struct因此堆栈分配变得不可能.

好处是:消耗代码永远不会依赖于定义struct,这意味着你不可能以某种方式struct从外部呈现不一致的内容,并且避免在struct更改时不必要地重新编译消耗代码.

第一个问题在通过声明字段来解决private.但是class仍然会在使用它的所有编译单元中导入您的定义,这使得有必要重新编译它们,即使只有您的private成员发生更改.在经常使用的解决方案是pimpl模式:将所有私有成员放在第二个struct(或class:)中,仅在实现文件中定义.当然,这需要pimpl在堆上分配.

除此之外:现代 OOP语言(例如)具有分配对象(通常在内部决定它是堆栈还是堆)的方法,而不需要调用代码知道它们的定义.

  • 历史上有时有用的一种技术是拥有一个包含适当大小的“int[]”的“公共”结构类型和包含实际数据的内部使用结构类型。客户端代码仍然有必要知道结构的大小,但是客户端代码将屏蔽内部结构。然而,我不知道有什么方法可以在不要求标准未强制执行的行为的情况下实现这一目标,并且当今的一些编译器试图“优化”标准不要求它们保留的任何内容。 (2认同)