在c中,具有专用于分配内存的功能是不是很糟糕?

时间:2017-10-26 14:13:13

标签: c memory-management

我组织了一个带有分配内存的函数的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函数不是标准的原因。所以我会说第二种选择是最好的?

4 个答案:

答案 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)

这实际上是一个非常好的问题,这里有很多事情需要考虑。以下是好的设计/良好实践:

  1. 私有数据封装。
  2. 将分配与算法分开。
  3. 避免类/ ADT与其分配方法之间的紧密耦合。
  4. 以上都不是主观的,这些都被普遍认为是好的设计。

    在C中,很难同时获得所有这些。对于简单的应用程序,通常可以跳过上述部分内容。对于更大,更复杂的应用程序,您肯定需要1)。

    • 您的第一个示例example * example_init()状态2) - 保持初始化和分配在一起是很好的设计。但它没有说明3)。并且它可能也不是1),除非您将此结构实现为 opaque类型
    • 你的第二个例子是void example_init(example *p_example) sates 2)和3),但可能不是1)。

      你可以重写第二个例子以满足所有三个,但最后你得到了两个函数。代码用户调用分配函数和初始化函数是很尴尬的 - 这不是理想的API设计。

    另一方面,不透明类型通常被认为是C中最好的设计实践 - 它是在C中实现真正的私有封装同时保持代码重入的唯一方法。但是当您使用opaque类型时,调用者无法再进行分配。因此,当您使用opaque类型时,几乎不会有3)。

    对于拥有操作系统的托管系统,动态分配通常是事实上的标准分配,并且始终使用动态分配并不是问题,3)并不是一个大问题。但是,在独立式嵌入式系统中,禁止动态分配,因此如果将代码与动态分配相结合,则不适合嵌入式系统。

    所以你的问题的答案是:它取决于。

    如果您的程序已经没有太多的私有封装方式,那么将分配留给调用者绝对是最好的方法。但另一方面,缺乏私人封装可能是一个主要的设计缺陷。

答案 3 :(得分:0)

作为一般规则,如果内存分配很复杂,只有一个分配内存的函数(例如,你的struct包含需要分配的指针)。在这种情况下,具有解除分配的对称功能。像cairo和openSSL这样的库有这种模式。

否则让用户决定如何创建结构(使用malloc或在堆栈上创建它)。

如果你有一个创建函数,它总是有一个对称的销毁函数,因为它告诉用户他必须自己销毁这个对象。它还确保使用正确的堆进行解除分配(dlls / so可能有自己的堆)。