【问题标题】:Using function parameter as variable使用函数参数作为变量
【发布时间】:2017-12-21 08:23:07
【问题描述】:

在函数内部重新分配函数参数是不好还是好的做法,或者可能是未定义的行为?

让我用一个例子解释一下我想做什么,这里是函数:

void
gkUpdateTransforms(GkNode *node /* other params */) {
  GkNode *nodei;

  if (!(nodei = node->chld))
    return;

  do {
    /* do job */
    nodei = nodei->next;
  } while (nodei);
}

替代方案:

void
gkUpdateTransforms2(GkNode *node /* other params */) {

  /* node parameter is only used here to get chld, not anywhere else */
  if (!(node = node->chld))
    return;

  do {
    /* do job */
    node = node->next;
  } while (node);
}

我检查了汇编输出,它看起来一样,我们不需要在第二个中声明变量。你可能会问,如果参数类型改变了,但第一个条件相同,因为它也需要更新。

编辑:参数是传值,我的目的不是编辑指针本身

EDIT2:递归函数呢?如果 gkUpdateTransforms2 是递归的会发生什么?我很困惑,因为函数会调用自己,但我认为在每次调用中,参数都会是不同的堆栈

【问题讨论】:

  • 该参数是函数的本地参数。修改 its 值与修改任何本地值一样好或坏。如果您以某种方式使其具有不确定的值,则它只是 UB。
  • 在函数内部修改函数的参数是完全可以的。这一点也不坏。
  • 完全没问题。当然不是未定义的行为。
  • "如果你以某种方式让它取一个不确定的值,它只是 UB" 我不知道该怎么做?但根据 cmets 看来我在安全区域
  • 在格式良好的程序中无法做到这一点。无论哪种方式,都要像对待任何局部变量一样对待它。你没有做坏事。

标签: c function c99 c89 function-parameter


【解决方案1】:

我不知道您为什么认为这是未定义的行为 - 它不是。主要是编码风格的问题,没有明显的对错。

通常,将参数视为immutable objects 是一种很好的做法。保留函数输入的未修改副本很有用。出于这个原因,使用只是参数副本的局部变量可能是个好主意。如您所见,这丝毫不会影响性能——编译器会优化代码。

但是,如果您写入参数也没什么大不了的。这也是常见的做法。将其称为不好的做法是非常迂腐的。

一些迂腐的编码风格使所有函数参数const如果它们不应该被修改,但我个人认为这只是混淆,这使得代码更难阅读。在您的情况下,这种迂腐风格将是void gkUpdateTransforms(GkNode*const node)。不要与 const 正确性混淆,这是一件普遍的好事,而不仅仅是风格问题。


但是,您的代码中有一些东西绝对被认为是不好的做法,那就是条件内的赋值。尽可能避免这种情况,这很危险并且会使代码更难阅读。大多数情况下没有任何好处。

混淆 === 的危险在 C 的早期就已经被注意到了。为了解决这个问题,在 1980 年代人们想出了像 "yoda conditions" 这样的大脑受损的东西。然后在 1989 年左右出现了 Borland Turbo C,它有一个奇特的警告功能“可能不正确的分配”。这就是 Yoda 条件的死亡,从那时起编译器就警告不要在条件中赋值。

确保您当前的编译器对此给出警告。也就是说,确保不要使用比 1989 年以来的 Borland Turbo 更差的编译器。是的,市场上有更差的编译器。

(gcc 给出“警告:建议在赋值周围使用括号作为真值”)


我会把代码写成

void gkUpdateTransforms(GkNode* node /* other params */) 
{
  if(node == NULL)
  {
    return ;
  }

  for(GkNode* i=node->chld; i!=NULL; i=i->next;)
  {
    /* do job */
  }
}

这主要是为了使代码更具可读性而进行的样式更改。它并没有提高性能。

【讨论】:

  • 我敢打赌,您说的是汽车行业中的某些编译器,这些编译器巧合地专门用于安全关键系统 - 可能会出错! ;)
  • @Vroomfondel 确实,我主要指的是各种嵌入式系统编译器。
  • 您似乎是一个明显的例外 - 我的大多数客户都将软件工具角的技术视为给定的。你在欧洲吗?
  • 一些技术/替代方案通过破解编译器来工作。我必须确保这不是 hack,这就是为什么我问它是否是 UB,就像 @Vroomfondel 回答下一个编译器可能会改变这种行为(我现在没有),这就是为什么问它是否是坏/好练习...
  • 还在此处讨论的条件中分配变量:stackoverflow.com/questions/151850/… 我认为它不会降低代码的可读性,我认为编译器会将其解释为类似的代码,但是好的,我会调查它。 PS:我已经在分配周围使用括号
【解决方案2】:

恕我直言,这并不是完全“坏”的做法,但如果没有更好的方法,值得质疑自己。关于您分析汇编程序输出:它可能作为幕后有趣且具有教育意义的外观,但不建议您将此作为优化的理由,或者更糟糕的是,源代码中的懒惰。下一个编译器或下一个架构可能只会让你的想法完全无效——我的建议是留在 Knuth:"Instead of imagining that our main task is to instruct a computer what to do, let us concentrate rather on explaining to human beings what we want a computer to do."

在您的代码中,我认为决定是 50:50,没有明确的赢家。我认为节点迭代器是一个自己的概念,证明了一个单独的编程结构(在我们的例子中只是一个变量)但是这个函数又是如此简单,以至于我们在下一个清晰度方面并没有赢得太多程序员正在查看您的代码,因此我们可以很好地使用第二个版本。如果你的函数随着时间的推移开始变异和增长,这个前提可能会变得无效,我们最好使用第一个版本。 也就是说,我会像这样编写第一个版本:

void
gkUpdateTransforms(GkNode *node /* other params */) {    
  for (GkNode *nodei = node->chld; nodei != NULL; nodei = nodei->next) {
      /* do job */
  }
}

【讨论】:

    【解决方案3】:

    这是很好的定义,也是实现这种行为的绝佳方式。

    您可能将其视为问题的原因是执行以下操作的常见错误:

    int func(object a) {
        modify a // only modifying copy, but user expects a to be modified
    

    但在您的情况下,您希望制作指针的副本。

    【讨论】:

    • 我更新了问题以涵盖递归函数,它们呢?
    • @recp 关于递归:没问题,函数的每次调用都会得到它自己的参数副本。函数参数与局部变量完全一样,只是它在函数的开头被初始化。
    • 在递归调用中使用函数参数是创建值堆栈的常用方法。因为每个函数调用都会创建一个新的堆栈副本,一些程序员利用这一点来创建一个伪堆栈,允许使用当前值对连续递归调用产生的值进行操作。
    • @recp 递归函数没有区别 - 参数仍然是本地副本。现在当然绝对没有必要在这里使用递归,你从这里递归得到的唯一好处是:执行速度变慢,内存使用更多,代码可读性降低,堆栈溢出的危险。
    【解决方案4】:

    只要它是按值传递的,它就可以安全地被视为任何其他局部变量。在这种情况下不是一个坏习惯,也不是未定义的行为。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-01-11
      • 2011-02-14
      • 1970-01-01
      • 2013-12-23
      • 2014-12-28
      • 2016-05-01
      • 2014-02-03
      相关资源
      最近更新 更多