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

时间:2015-07-28 10:00:58

标签: c struct stack heap

每当我看到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中似乎是可以接受的。是什么原因背后为什么堆分配的struct"类"很常见吗?

12 个答案:

答案 0 :(得分:50)

这有几个原因。

  1. 使用“不透明”指针
  2. 缺乏破坏者
  3. 嵌入式系统(堆栈溢出问题)
  4. 容器
  5. 惯性
  6. “懒惰”
  7. 让我们简要讨论一下。

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

    struct CClass_;
    typedef struct CClass_ CClass;
    // the rest as in your example
    

    因此,用户没有看到struct CClass_的定义,使她无法对其进行更改并启用其他有趣的内容,例如为不同平台实现不同的类。

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

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

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

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

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

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

    CClass CClass_make();
    void CClass_deinit(CClass *me);
    

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

    CClass *CClass_create();
    void CClass_destroy(CClass *me);
    

    这对实施者来说还有很多工作要做,但也让用户感到困惑。可以使接口略有不同,但它不会改变您需要两组功能的事实。

    当然,“容器”的原因也部分是“懒惰”的原因。

答案 1 :(得分:14)

假设,在您的问题中CClass_createCClass_destroy使用malloc/free,那么对我来说,做以下操作是不好的做法:

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

  CClass_destroy(myinstance);
}

因为我们可以轻松避免使用malloc和免费:

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

答案 2 :(得分:9)

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

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

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

答案 3 :(得分:8)

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

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

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

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

答案 4 :(得分:3)

一般情况下,您看到*这一事实并不意味着它已malloc'。例如,你可能有一个指向static全局变量的指针;事实上,在你的情况下,CClass_destroy()没有采取任何参数,假设它已经知道有关被销毁对象的一些信息。

此外,指针,无论是否malloc',都是允许您修改对象的唯一方法。

我没有看到使用堆而不是堆栈的特殊原因:使用的内存不会减少。但是,初始化这样的“类”需要的是init / destroy函数,因为底层数据结构可能实际上需要包含动态数据,因此使用指针。

答案 5 :(得分:3)

我会将“构造函数”更改为void CClass_create(CClass*);

它不会返回结构的实例/引用,而是在其中调用。

至于它是在“堆栈”上还是动态分配,它完全取决于您的使用场景要求。无论如何分配它,只需调用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*);(该对象的生命周期结束),如果动态分配,也释放该内存。

区分分配/释放和构造/破坏,那些不一样(即使在C ++中它们可能会自动耦合在一起)。

答案 6 :(得分:2)

C缺乏C ++程序员理所当然的某些事情。

  1. 公共和私人说明者
  2. 构造函数和析构函数
  3. 这种方法的一大优点是你可以在你的C文件中隐藏结构,并使用你的create和destroy函数强制正确构造和破坏。

    如果在.h文件中公开结构,这意味着用户可以直接访问成员,从而打破封装。也不强制创建允许错误构造对象。

答案 7 :(得分:2)

因为函数只能返回堆栈分配的结构,如果它不包含指向其他已分配结构的指针。 如果它只包含简单对象(int,bool,float,chars和它们的数组,但是没有指针),你可以在堆栈上分配它。但是你必须知道,如果你退回它,它将被复制。如果你想允许指向其他结构的指针,或者想要避免副本,那么使用堆。

但是如果你可以在顶级单元中创建结构并且只在被调用的函数中使用它并且永远不会返回它,那么堆栈是合适的

答案 8 :(得分:2)

如果需要同时存在的某种类型的最大对象数量是固定的,系统将需要能够对每个“实时”实例执行某些操作,并且所讨论的项目不会消耗太多钱,最好的方法通常既不是堆分配也不是堆栈分配,而是静态分配的数组,以及“create”和“destroy”方法。使用数组将避免维护链接的对象列表的需要,并且可以处理不能立即销毁对象的情况,因为它“忙”[例如如果数据通过中断或DMA通过通道到达,当用户代码决定它不再对通道感兴趣并处理它时,用户代码可以设置“完成后处置”标志并返回而不必担心有待处理的中断或DMA覆盖不再分配给它的存储]。

使用固定大小的固定大小的对象池使得分配和取消分配比从混合大小的堆中获取存储更加可预测。在需求变化且对象占用大量空间(单独或共同)但需求大部分一致的情况下(例如,应用程序始终需要12个对象,有时需要最多3个),这种方法并不是很好更多)它可以比其他方法更好地工作。唯一的缺点是任何设置必须在声明静态缓冲区的地方执行,或者必须由客户端中的可执行代码执行。无法在客户端站点使用变量初始化语法。

顺便说一句,使用这种方法时,不需要客户端代码接收任何指针。相反,人们可以使用任何大小的整数方便地识别资源。此外,如果资源的数量永远不会超过int中的位数,则某些状态变量每个资源使用一位可能会有所帮助。例如,可以有变量timer_notifications(仅通过中断处理程序写入)和timer_acks(仅通过主线代码编写)并指定只要计时器N想要设置(timer_notifications ^ timer_acks)的位N服务。使用这种方法,代码只需读取两个变量来确定是否有任何计时器需要服务,而不必为每个计时器读取一个变量。

答案 9 :(得分:1)

你的问题是“为什么在C中动态分配内存是正常的,在C ++中它不是”?

C ++有很多构造可以使新的冗余。 复制,移动和普通构造函数,析构函数,标准库,分配器。

但是在C中你无法解决它。

答案 10 :(得分:1)

它实际上是对C ++的反对,使“新”变得太容易了。

理论上,在C中使用这个类构造模式与在C ++中使用“new”相同,所以应该没有区别。然而,人们倾向于考虑语言的方式是不同的,所以人们对代码的反应方式是不同的。

在C中,考虑计算机为实现目标必须采取的确切操作是很常见的。它不是普遍的,但它是一种非常普遍的心态。假设您已花时间对malloc / free进行成本/收益分析。

在C ++中,编写代码行更加容易,这对您来说很有帮助,而您甚至没有意识到这一点。有人写一行代码是很常见的,甚至没有意识到它恰好要求100或200个新/删除!这引起了强烈反对,C ++开发人员会狂热地对新闻和删除行为进行挑剔,因为担心他们会在整个地方被意外调用。

这些当然是概括。整个C和C ++社区绝不适合这些模具。但是,如果你在使用new而不是把东西堆放在堆上,那么这可能是根本原因。

答案 11 :(得分:0)

It is rather strange that you see it so often. You must have been looking as some "lazy" code.

In C the technique that you describe is typically reserved to "opaque" library types, i.e. struct types whose definitions are intentionally made invisible to the client's code. Since the client cannot declare such objects, the idiom has to really on dynamic allocation in the "hidden" library code.

When hiding the definition of the struct is not required, a typical C idiom usually looks as follows

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

CClass* CClass_init(CClass* cclass);
void CClass_release(CClass* cclass);

Function CClass_init initializes the *cclass object and returns the same pointer as result. I.e. the burden of allocating memory for the object is placed on the caller and the caller can allocate it in any way it sees fit

CClass cclass;
CClass_init(&cclass);
...
CClass_release(&cclass);

A classic example of this idiom would be pthread_mutex_t with pthread_mutex_init and pthread_mutex_destroy.

Meanwhile, using the former technique for non-opaque types (as in your original code) is generally a questionable practice. It is exactly questionable as gratuitous use of dynamic memory in C++. It works, but again, using dynamic memory when it is not required is as frowned upon in C as it is in C++.