我组织了一个带有分配内存的函数的c项目
例如,我得到了这个结构:
typedef struct t_example{
int x;
int y;
}example;
我已经创建了这个函数来初始化一个实例:
/**
return value must be freed
*/
example * example_init(){
example *p_example = malloc(sizeof(example));
p_example->x = 42;
p_example->y = 42;
return p_example;
}
我可以像这样调用这个函数
example *p_example = example_init();
但随着我的项目的增长,我发现有时我不需要分配内存,如果我只需要在堆栈上有一个局部变量,但需要初始化它,所以我将init函数更改为:
void example_init(example *p_example){
p_example->x = 42;
p_example->y = 42;
}
所以我可以像这样调用这个函数
example o_example;
example_init(&o_example);
当然,如果我有一个指针
,这个功能也可以工作example *p_example = malloc(sizeof(example));
example_init(p_example);
我的问题是:哪种是最佳做法: 1)提供一个将分配内存的函数(并正确地记录这个),因为它可能很方便,或者2)这应该留给函数的调用者。
我还读到std函数没有动态分配内存,这就是strdup函数不是标准的原因。所以我会说第二种选择是最好的?
答案 0 :(得分:5)
我的问题是:最好的做法是:1)提供一个能够分配内存的功能(并正确记录这个),因为它可能很方便,或者2)这应该留给调用者功能。
我不认为这是最佳做法的问题。创建并返回(指向)新的动态分配对象的函数没有任何内在错误。为了比直接分配空间更有用,这样的函数应该确保为对象提供一致的初始值,尽管它可能通过调用不同的函数来实现。总的来说,这是C ++的new
运算符与构造函数相结合的C模拟。
这并不排除用户自己分配对象,无论是动态还是其他方式。如果有问题的类型是公共的,那么可能有充分的理由提供不进行分配的初始化函数。正如您所观察到的,这特别适用于依赖于自动或静态分配对象的代码。
我还读到std函数没有动态分配内存,这就是strdup函数不是标准的原因。所以我会说第二种选择是最好的?
标准委员会的标准库功能政策无法合理地扩展到您自己的职能部门。最终结果是任何地方都不能动态分配内存,如果这是委员会的意图,那么他们至少会弃用标准库的显式内存分配函数。
答案 1 :(得分:3)
在处理非标量类型时,将分配,释放和初始化抽象到自己的函数中总是一个好主意。当您必须按特定顺序分配和取消分配多个资源时,它特别有用。
使用单独的函数进行分配和初始化:
example *example_create( void )
{
example *p = malloc( sizeof *p );
if ( !p )
log_error(); // or not - up to you
return p;
}
void example_init( example *p )
{
p->x = p->y = 42;
}
example *new_example = example_create( );
if ( new_example )
example_init( new_example );
为了增加一些灵活性,您可以将初始化程序作为回调传递给分配器:
example *example_create( void (*example_initializer)(example *) )
{
example *p = malloc( sizeof *p );
if ( p )
if ( example_initializer )
example_initializer( p );
return p;
}
这样,您可以将分配和初始化组合到一个操作中,但仍然保持分配和初始化解耦:
void init42( example *p ) { p->x = p->y = 42; }
void init0( example *p ) { p->x = p->y = 0; }
void initRand( example *p ) { p->x = rand(); p->y = rand(); }
example *p42 = example_create( init42 );
example *p0 = example_create( init0 );
example *pRand = example_create( initRand );
而且,您仍然可以将初始化程序与auto
变量一起使用:
example instance42;
init42( &instance42 );
example instance0;
init0 ( &instance0 );
example instanceRand;
initRand( &instanceRand );
答案 2 :(得分:1)
这实际上是一个非常好的问题,这里有很多事情需要考虑。以下是好的设计/良好实践:
以上都不是主观的,这些都被普遍认为是好的设计。
在C中,很难同时获得所有这些。对于简单的应用程序,通常可以跳过上述部分内容。对于更大,更复杂的应用程序,您肯定需要1)。
example * example_init()
状态2) - 保持初始化和分配在一起是很好的设计。但它没有说明3)。并且它可能也不是1),除非您将此结构实现为 opaque类型。你的第二个例子是void example_init(example *p_example)
sates 2)和3),但可能不是1)。
你可以重写第二个例子以满足所有三个,但最后你得到了两个函数。代码用户调用分配函数和初始化函数是很尴尬的 - 这不是理想的API设计。
对于拥有操作系统的托管系统,动态分配通常是事实上的标准分配,并且始终使用动态分配并不是问题,3)并不是一个大问题。但是,在独立式嵌入式系统中,禁止动态分配,因此如果将代码与动态分配相结合,则不适合嵌入式系统。
所以你的问题的答案是:它取决于。
如果您的程序已经没有太多的私有封装方式,那么将分配留给调用者绝对是最好的方法。但另一方面,缺乏私人封装可能是一个主要的设计缺陷。
答案 3 :(得分:0)
作为一般规则,如果内存分配很复杂,只有一个分配内存的函数(例如,你的struct包含需要分配的指针)。在这种情况下,具有解除分配的对称功能。像cairo和openSSL这样的库有这种模式。
否则让用户决定如何创建结构(使用malloc或在堆栈上创建它)。
如果你有一个创建函数,它总是有一个对称的销毁函数,因为它告诉用户他必须自己销毁这个对象。它还确保使用正确的堆进行解除分配(dlls / so可能有自己的堆)。