C中结构成员(指针与数组)的内存分配之间的差异
Difference between memory allocations of struct member (pointer vs. array) in C
这两种分配内存的方式有什么有效的区别吗?
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;
}
据我了解,整个 Container
结构将被分配到堆中,而 buffer
将指向相同的结构。这是正确的吗?
有区别。
我将尝试说明示例。
正如其他人指出的那样:
- 第一个示例更难管理,但您可以随时更改缓冲区的大小。
- 第二个例子更容易管理(你不必关心单独释放缓冲区),但你只能有固定大小的缓冲区。
正如评论中所指出的:如果缓冲区是结构中的最后一个元素(如提供的示例中所示),则可以为缓冲区分配任何长度。
例如
int extra_bytes_needed = ...;
Container* container = calloc(sizeof(Container) + extra_bytes_needed, 1);
图片左侧 - 您的第一个案例。
在图片的右侧-您的第二个案例。
Vladislav 很好地说明了区别;但是这是什么意思?不同的内存组织有几个影响:
第二个例子中的struct container
可用as-is;它不需要任何初始化。例如。
typedef struct {
uint8_t buffer[4];
} Container;
Container c;
strcpy(c.buffer, "Yes");
很好,但它可能会在第一个版本中崩溃,因为指针 c.buffer
将未初始化并包含无效地址。
- 具有 built-in 缓冲区的结构的性能可能更好,因为每个
init()
节省了一次分配。内存局部性也可能是一个问题:通过动态分配,缓冲内存可能远离结构内存,因此它不在缓存中。
还有一点。您在这里模仿 C++,init()
扮演带有构造函数的工厂的角色。
不幸的是,只要 struct Container
的定义可见,任何用户都可以创建一个未初始化的 Container
并使用它,这会带来灾难性的后果。 (在 C++ 中,我们会将构造函数声明为私有的,但在 C 中不能这样做。)
阻止用户创建 struct Container
的唯一方法是隐藏其实现。这类似于 C++ Pimpl idiom: The user has no header which actually defines Container
but only a header defining operations on it which take and return 指向 到 Container
(就像你的 init()
)。就用户而言,Container
保持 不完整类型。
这是一个例子。此版本的容器具有以下功能:
它不提供对数据的直接访问,而是分发数据的副本。这是否是可接受的开销取决于用例。我只是想说明 我们需要对 Container 的零知识。 它是完全隐藏的。缺少 re-engineering 类型,除非通过其官方界面,否则根本无法操作内容。 (这可能是一个缺点。)
实际缓冲区(以及大小)现在是动态的。用户数据大小的唯一限制是由系统强加的。
容器在用户获取容器中数据的副本时为用户分配内存,类似于POSIXscanf
"assignment-allocation character"'m'.
容器为分配的内存量和用户数据实际占用的内存量维护不同的大小。这避免了不必要的 re-allocations.
用户看到的容器是这个 header 和 collection 的函数签名:
#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
一个简单的使用例子:
#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.
}
最后,(隐藏,通常在仅链接的库中)Container 及其 "member" 函数的实现如下所示:
#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 */
这两种分配内存的方式有什么有效的区别吗?
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;
}
据我了解,整个 Container
结构将被分配到堆中,而 buffer
将指向相同的结构。这是正确的吗?
有区别。
我将尝试说明示例。
正如其他人指出的那样:
- 第一个示例更难管理,但您可以随时更改缓冲区的大小。
- 第二个例子更容易管理(你不必关心单独释放缓冲区),但你只能有固定大小的缓冲区。
正如评论中所指出的:如果缓冲区是结构中的最后一个元素(如提供的示例中所示),则可以为缓冲区分配任何长度。
例如
int extra_bytes_needed = ...;
Container* container = calloc(sizeof(Container) + extra_bytes_needed, 1);
图片左侧 - 您的第一个案例。
在图片的右侧-您的第二个案例。
Vladislav 很好地说明了区别;但是这是什么意思?不同的内存组织有几个影响:
-
第二个例子中的
struct container
可用as-is;它不需要任何初始化。例如。
typedef struct {
uint8_t buffer[4];
} Container;
Container c;
strcpy(c.buffer, "Yes");
很好,但它可能会在第一个版本中崩溃,因为指针 c.buffer
将未初始化并包含无效地址。
- 具有 built-in 缓冲区的结构的性能可能更好,因为每个
init()
节省了一次分配。内存局部性也可能是一个问题:通过动态分配,缓冲内存可能远离结构内存,因此它不在缓存中。
还有一点。您在这里模仿 C++,init()
扮演带有构造函数的工厂的角色。
不幸的是,只要 struct Container
的定义可见,任何用户都可以创建一个未初始化的 Container
并使用它,这会带来灾难性的后果。 (在 C++ 中,我们会将构造函数声明为私有的,但在 C 中不能这样做。)
阻止用户创建 struct Container
的唯一方法是隐藏其实现。这类似于 C++ Pimpl idiom: The user has no header which actually defines Container
but only a header defining operations on it which take and return 指向 到 Container
(就像你的 init()
)。就用户而言,Container
保持 不完整类型。
这是一个例子。此版本的容器具有以下功能:
它不提供对数据的直接访问,而是分发数据的副本。这是否是可接受的开销取决于用例。我只是想说明 我们需要对 Container 的零知识。 它是完全隐藏的。缺少 re-engineering 类型,除非通过其官方界面,否则根本无法操作内容。 (这可能是一个缺点。)
实际缓冲区(以及大小)现在是动态的。用户数据大小的唯一限制是由系统强加的。
容器在用户获取容器中数据的副本时为用户分配内存,类似于POSIX
scanf
"assignment-allocation character"'m'.容器为分配的内存量和用户数据实际占用的内存量维护不同的大小。这避免了不必要的 re-allocations.
用户看到的容器是这个 header 和 collection 的函数签名:
#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
一个简单的使用例子:
#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.
}
最后,(隐藏,通常在仅链接的库中)Container 及其 "member" 函数的实现如下所示:
#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 */