Creating "classes" in C, on the stack against the heap?

Whenever I see C "class" (any structure that is intended to be used when accessing functions that take a pointer to it as the first argument), I see that they are implemented as follows:

typedef struct { int member_a; float member_b; } CClass; CClass* CClass_create(); void CClass_destroy(CClass *self); void CClass_someFunction(CClass *self, ...); ... 

And in this case CClass_create always malloc is memory and returns a pointer to it.

Whenever I see that new appears in C ++ unnecessarily, it usually drives C ++ programmers crazy, but this practice seems acceptable in C. What gives? Is there any reason why the so-called "class classes" allocated by the heap are so common?

+46
c stack heap struct
Jul 28 '15 at 10:00
source share
12 answers

There are several reasons for this.

  • Using Opaque Pointers
  • Lack of destructors
  • Embedded systems (problem)
  • Containers
  • Inertia
  • "Laziness"

Briefly discuss them.

For opaque pointers, it allows you to do something like:

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

Thus, the user does not see the definition of struct CClass_ , isolating it from changes to it and allowing other interesting materials, for example, to implement the class differently for different platforms.

Of course, this prohibits the use of CClass stack CClass . But, OTOH, you can see that this does not prohibit the allocation of CClass objects statically (from some pool) - CClass_create is returned CClass_create or perhaps another function of the CClass_create_static type.

Lack of destructors - since the C compiler will not automatically destroy your CClass stack CClass , you need to do this yourself (manually calling the destructor function). Thus, the only advantage is the fact that stack distribution is generally faster than heap distribution. OTOH, you don’t need to use a bunch - you can allocate from a pool or arena or something like that, and it can be almost as fast as stack distribution, without the possible stack distribution problems discussed below.

Embedded Systems - The stack is not an “endless” resource, you know. Of course, for most applications on today's "regular" OS (POSIX, Windows ...) this is almost the case. But on embedded systems, the stack can be only a few KB. These extreme, but even “large” embedded systems have a stack that resides in MB. Thus, it will be exhausted if used too much. When this happens, there is basically no guarantee what will happen - AFAIK, in both C and C ++, that the "Undefined behavior". OTOH, CClass_create() can return a NULL pointer when you have lost memory, and you can handle it.

Containers are C ++ users, such as stack distribution, but if you create the std::vector stack on the stack, its contents will be allocated to the heap. You can configure this, of course, but this is the default behavior, and it makes life easier to say that "all container members are allocated as heaps" rather than trying to figure out how to handle if they are not.

Inertia - well, OO came from SmallTalk. Everything is dynamic there, so the "natural" translation into C is to put everything on a heap. So, the first examples were like that, and they inspired others for years to come.

" Laziness " - if you know that you want only stack objects, you need something like:

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

But if you want to allow both stack and heap, you need to add:

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

This is more work for the developer, but also confuses the user. You can make slightly different interfaces, but this does not change the fact that you need two sets of functions.

Of course, the reason for "containers" is also partly the reason for "laziness."

+49
Jul 28 '15 at 10:51
source share

Assuming that, as in your question, CClass_create and CClass_destroy use malloc/free , then for me the following: wrong practice:

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

because we could easily get rid of malloc and freely:

 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 } 

from

 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 ... } 

In C ++, we would also prefer to do this:

 void Myfunc() { CClass myinstance; ... } 

than this:

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

To avoid unnecessary new / delete .

+14
Jul 28 '15 at 10:13
source share

In C, when a component provides the "create" function, the component developer also controls the component initialization process. Therefore, it not only emulates C ++ ' operator new , but also a class constructor.

Providing this control over initialization means a much larger check for input errors, so maintaining control makes it easier to ensure consistent and predictable behavior.

I also make an exception for malloc always used to allocate memory. This is often the case, but not always. For example, on some embedded systems, you will find that malloc / free not used at all. X_create functions can be allocated in other ways, for example. from an array whose size is fixed at compile time.

+9
Jul 28 '15 at 10:17
source share

This begets a lot of answers because it is somewhat opinion based. However, I want to explain why I personally prefer my “C objects” to be heaped. The reason is that my fields are hidden (they say: private) from code consumption. This is called an opaque pointer. In practice, this means that your header file does not define the struct used, it only declares it. As a direct consequence, the consumer code cannot know the size of the struct , and therefore stack distribution becomes impossible.

Advantage: consumer code can never depend on the definition of struct , which means you cannot somehow make the contents of struct inconsistent from the outside and avoid unnecessarily recompiling the consumption code when changing struct .

The first problem is solved in C ++ by declaring private fields. But the definition of your class is still imported into all compilation units that use it, which makes it necessary to recompile them even when only private members change. The solution often used in C ++ is the pimpl template: all private members in the second struct (or: class ), which is defined only in the implementation file, have it. Of course, this requires your pimpl be allocated on the heap.

Adding to this: modern OOP languages ​​(for example, java or C # ) have means for allocating objects (and, as a rule, decide whether it will be a stack or a heap inside) without calling code, knowing about their definition.

+7
Jul 28 '15 at 11:40
source share

In general, the fact that you see * does not mean that it was malloc 'd. For example, you could get a pointer to a global variable static ; in your case, indeed, CClass_destroy() does not accept any parameter that assumes that it already knows some information about the destroyed object.

In addition, pointers, whether or not they are malloc , are the only way that you can modify an object.

I see no particular reason for using heap instead of stack: you are not getting less memory used. However, to initialize such “classes”, it is necessary to initialize / destroy functions, because the main data structure can actually contain dynamic data, therefore the use of pointers.

+3
Jul 28 '15 at 10:11
source share

I would change the "constructor" to void CClass_create(CClass*);

It will not return an instance / link of the structure, but will be called on one.

Depending on whether it is allocated on the "stack" or dynamically, it completely depends on the requirements for the use case. However, you allocate it, you simply call CClass_create() passing the selected structure as a parameter.

 { 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 

Just be careful not to return a link to the local one (allocated on the stack), because this is UB.

However, you select it, you will need to call void CClass_destroy(CClass*); in the right place (the end of this lifetime of the object that is), and if dynamically allocated, also free this memory.

To distinguish between distribution / release and construction / destruction, those that are not the same (even if in C ++ they can automatically join together).

+3
Jul 28 '15 at 10:39
source share

C lacks certain things that C ++ programmers take for granted, namely:

  • public and private specifiers
  • constructors and destructors

The big advantage of this approach is that you can hide the structure in your C file and force the correct design and destruction to perform its creation and destruction functions.

If you find structure in your .h file, this means that users can access elements that directly violate encapsulation. Also, not forcing the creation does not allow you to create the wrong design of your object.

+2
Jul 28 '15 at 10:13
source share

Since a function can only return a structure selected by the stack, if it does not contain pointers to other selected structures. If it contains only simple objects (int, bool, floats, chars and arrays of them, but without a pointer ), you can select it on the stack. But you should know that if you return it, it will be copied. If you want to allow pointers to other structures or want to avoid copying, use a bunch.

But if you can create a structure in a top-level block and use it only in called functions and never return it, then the corresponding stack will be

+2
Jul 28 '15 at 10:28
source share

If the maximum number of objects of any type that must exist simultaneously is fixed, the system will have to do something with each “live” instance, and the objects in question do not consume too much money, the best approach is usually not to allocate a heap, but to distribute the stack , but rather a statically distributed array, as well as create and destroy methods. Using an array will avoid the need to maintain a linked list of objects and will allow you to handle the case when the object cannot be immediately destroyed because it is "busy" (for example, "busy"). if the data arrives on the channel through an interrupt or DMA, when the user code decides that it is no longer interested in the channel and disposes of it, the user code can set the “dispose when done” flag and return without worrying about the presence of the waiting interrupt, or the DMA overwrites the storage that no longer stands out for him].

Using a fixed-size pool of fixed-size objects makes allocation and de-allocation much more predictable than storing storage from a mixed-size heap. This approach is small in cases where demand is variable and objects take up a lot of space (individually or collectively), but when demand is basically agreed (for example, an application requires 12 objects all the time, and sometimes it takes up to 3 more), it can work much better than alternative approaches. The only drawback is that any installation must be performed in the place where the static buffer is declared, or it must be executed by executable code in the clients. Cannot use initialization variable syntax on client site.

By the way, when using this approach, there is no need for client code to receive pointers to anything. Instead, you can define resources using any size of the whole. In addition, if the number of resources will never exceed the number of bits in an int , it may be useful for some state variables to use one bit per resource. For example, one could have the variables timer_notifications (written only through the interrupt handler) and timer_acks (written only with the main code) and indicate that the N bit from (timer_notifications ^ timer_acks) will be set when timer N wants to serve. Using this approach, the code should read only two variables to determine if any timer needs to be serviced, rather than reading one variable for each timer.

+2
Jul 28 '15 at 21:36
source share

Is your question "why in C normally allocate memory dynamically and in C ++ is it not?"

C ++ has many constructs that make new redundant. copy, move and normal constructors, destructors, standard library, allocators.

But in C you cannot get around this.

+1
Jul 28 '15 at 10:09
source share

This is actually a backlash in C ++, which makes the "new" too easy.

Theoretically, using this model for constructing a class in C is identical to using the “new” in C ++, so there should be no difference. However, the way people tend to think about languages ​​is different, so the way people react to code is different.

In C, people often think about the exact operations that a computer will need to do to achieve your goals. This is not universal, but it is a very common thinking. It is assumed that you have spent time on cost-benefit analysis for malloc / free.

In C ++, it has become much easier to write lines of code that are a lot to you, without your understanding. For someone, quite often writing a line of code and not even realize that this caused 100 or 200 new / deletions! This caused a backlash when the C ++ developer fanatically obsessed with the news and removed them for fear that he accidentally calls them everywhere.

This, of course, is a generalization. By no means are all C and C ++ communities suitable for these forms. However, if you get flash using new ones instead of putting things in a heap, this may be the main reason.

+1
Jul 29 '15 at 4:31 on
source share

It is rather strange that you see so often. You must have looked like a "lazy" code.

In C, the method that you describe is usually reserved for “opaque” library types, that is, structure types whose definitions intentionally become invisible to client code. Since the client cannot declare such objects, the idiom must really be dynamically allocated in the "hidden" library code.

If hiding the definition of a structure is not required, a typical C-idiom usually looks like this

 typedef struct CClass { int member_a; float member_b; } CClass; CClass* CClass_init(CClass* cclass); void CClass_release(CClass* cclass); 

The CClass_init function initializes the *cclass object and returns the same pointer as the result. That is, the burden of allocating memory for an object is placed on the caller, and the caller can allocate it in any way that he 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 .

At the same time, using the previous technique of opaque types (as in the source code), as a rule, is a dubious practice. This is completely doubtful as the free use of dynamic memory in C ++. It works, but again, using dynamic memory when it is not needed, it frowned in C, as in C ++.

0
Jul 28 '15 at
source share



All Articles