问题
Is there any effective difference between these two styles of allocating memory?
1.
typedef struct {
uint8_t *buffer;
} Container;
Container* init() {
Container* container = calloc(sizeof(Container), 1);
container->buffer = calloc(4, 1);
return container;
}
2.
typedef struct {
uint8_t buffer[4];
} Container;
Container* init() {
Container* container = calloc(sizeof(Container), 1);
return container;
}
As far as I understand, the whole Container
struct will be heap allocated and buffer
will point to the same. Is this correct?
回答1:
There is a difference.
I'll try to illustrate the sample.
As others pointed out:
- First example is harder to manage, but you can change size of the buffer any time.
- Second example is easier to manage (you don't have to care about freeing buffer separately), but you can only have fixed size of buffer.
As pointed out in comments: In case if buffer is a last element in a structure (like in the example provided) it is possible to allocate any length for the buffer.
For example
int extra_bytes_needed = ...;
Container* container = calloc(sizeof(Container) + extra_bytes_needed, 1);
On the left side of image - your first case.
On the right side of image- your second case.
回答2:
Vladislav has illustrated the difference very nicely; but what does it mean? The different memory organization has a couple repercussions:
struct container
in the second example is usable as-is; it does not need any initialization. E.g.
typedef struct {
uint8_t buffer[4];
} Container;
Container c;
strcpy(c.buffer, "Yes");
is fine, but it would likely crash with the first version because the pointer c.buffer
would be uninitialized and contain an invalid address.
- The performance of a struct with a built-in buffer is probably better because one allocation is saved per
init()
. Memory locality can also be an issue: With dynamic allocation, the buffer memory is perhaps far away from the struct memory so that it is not in the cache.
One more point. You are mimicking C++ here, with init()
taking the role of a factory with constructor.
Unfortunately, as long as the definition of struct Container
is visible, any user can create an uninitialized Container
and use it, with disastrous consequences. (In C++ we would declare the constructor private, but we cannot do that in C.)
The only way to prevent a user from creating a struct Container
is by hiding its implementation. This resembles the C++ Pimpl idiom: The user has no header which actually defines Container
but only a header defining operations on it which take and return pointers to Container
(like your init()
). Container
stays an incomplete type as far as the user is concerned.
Here is an example. This version of a container has the following features:
It does not provide direct access to the data but instead hands out copies of the data. Whether that's an acceptable overhead depends on the use case. I just wanted to make the point that we need zero knowledge of Container. It is completely hidden. Short of re-engineering the type it is not possible at all to manipulate the contents except through its official interface. (That may be a disadvantage.)
The actual buffer (and hence the size) is now dynamic. The only limit for user data size is imposed by the system.
The container allocates memory for the user when the user obtains a copy of the data in the container, similar to the POSIX
scanf
"assignment-allocation character" 'm'.The container maintains separate sizes for the amount of allocated memory and how much of it is actually occupied by user data. This avoids unnecessary re-allocations.
What the user sees of a container is this header with a collection of function signatures:
#ifndef CONTAINER_INTERFACE_H
#define CONTAINER_INTERFACE_H
/* An abstract container. It can hold arbitrary amounts of data
by means of danamic allocation. An out-of-memory condition will make
it exit with an exit code of 1.
*/
#include <stddef.h> // size_t
/** Forward declaration, actual definition unknown */
struct Container;
typedef struct Container Container; // convenience
/** Create and initialize a Container of size 0.
*/
Container *ac_init();
/** Delete a Container and free its buffer */
void ac_dispose(Container *container);
/** Obtain the data in the given container. Note that we don't
expose the internal pointer to the user.
@param userBuf is a pointer a pointer
which will be set to an allocated memory area of sufficient
size. The user must free() it. If the container does not hold data,
*userBuf is not changed.
@return the number of bytes actually copied, which is also the
size of the allocated buffer.
*/
size_t ac_get(Container *container, unsigned char **userBuf);
/** Fill the container buffer with user data.
@return the number of bytes actually copied
*/
void ac_put(Container *container, const unsigned char *userData, size_t userDataSz);
/* ... (Many) more functions for more complicated structs */
#endif //ndef CONTAINER_INTERFACE_H
A simple use example:
#include <stdio.h>
#include <stdlib.h> // exit, malloc etc.
#include <string.h>
#include "container-interface.h"
/// Obtain a copy of the container data and print it.
void printContainerData(Container *c)
{
unsigned char *dataFromContainer; // will be set by ac_get
size_t contDataSz = ac_get(c, &dataFromContainer);
if(contDataSz == 0)
{
printf("[empty]\n");
}
else
{
dataFromContainer[contDataSz-1] = 0; // terminate string just in case.
printf("String from container: ->%s<-\n", (const char *)dataFromContainer);
free(dataFromContainer);
}
}
int main()
{
char *userInput; // will be set by scanf
Container *c = ac_init();
while(1) // exit by EOF (Ctrl-Z or Ctrl-D)
{
printf("Please enter a line (empty for exit) ->");
// EOF etc. will make scanf return something other than 1.
// Use the fancy "m" POSIX extension in the format string
// which allocates memory for us, obviating maximum line length
// considerations.
if(scanf("%m[^\n]", &userInput) != 1) { break; }
getchar(); // read away remaining newline
ac_put(c, (unsigned char *)userInput, strlen(userInput)+1);
printContainerData(c);
free(userInput);
}
ac_dispose(c); // kinda unnecessary in a hosted environment, but good habit.
}
Last, the (hidden, typically in a library which is only linked against) implementation of Container and its "member" functions looks like this:
#include <stdlib.h> // exit, malloc etc.
#include <string.h> // memcpy
#include "container-interface.h" // to make sure the function signatures match
/** The actual definition of Container. The user never sees this. */
struct Container
{
unsigned char *buf;
size_t dataSz;
size_t allocSz;
};
/** Create and initialize a struct Container */
struct Container *ac_init()
{
struct Container *newCont = malloc(sizeof(struct Container));
if(!newCont) { exit(1); } // out of mem
newCont->dataSz = 0;
newCont->allocSz = 0;
newCont->buf = NULL;
return newCont;
}
void ac_dispose(struct Container *container)
{
free(container->buf);
free(container);
}
size_t ac_get(struct Container *container, unsigned char **userBuf)
{
if(container->dataSz > 0)
{
*userBuf = malloc(container->dataSz);
if(!*userBuf) { exit(1); } // out of mem
memcpy(*userBuf, container->buf, container->dataSz);
}
return container->dataSz;
}
void ac_put(struct Container *container, const unsigned char *userData, size_t userDataSz)
{
if(userDataSz != 0)
{
if(container->allocSz < userDataSz)
{
free(container->buf);
container->buf = malloc(userDataSz);
if(!container->buf) { exit(1); } // out of mem
container->allocSz = userDataSz;
}
memcpy(container->buf, userData, userDataSz);
}
container->dataSz = userDataSz;
}
/* ... (Many) more functions for more complicated structs */
来源:https://stackoverflow.com/questions/61225421/difference-between-memory-allocations-of-struct-member-pointer-vs-array-in-c