在函数顶部声明变量还是在单独的作用域中声明变量?

50

哪一种更好,方法1还是方法2?

方法1:

LRESULT CALLBACK wpMainWindow(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    switch (msg)
    {
        case WM_PAINT:
        {
            HDC hdc;
            PAINTSTRUCT ps;

            RECT rc;
            GetClientRect(hwnd, &rc);           

            hdc = BeginPaint(hwnd, &ps);
            // drawing here
            EndPaint(hwnd, &ps);
            break;
        }
        default: 
            return DefWindowProc(hwnd, msg, wparam, lparam);
    }
    return 0;
}

方法二:

LRESULT CALLBACK wpMainWindow(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    RECT rc;

    switch (msg)
    {
        case WM_PAINT:
            GetClientRect(hwnd, &rc);

            hdc = BeginPaint(hwnd, &ps);
            // drawing here
            EndPaint(hwnd, &ps);
            break;

        default: 
            return DefWindowProc(hwnd, msg, wparam, lparam);
    }
    return 0;
}

在方法1中,如果当wpMainWindow函数被调用时msg = WM_PAINT,它是否会在开始时为堆栈上的所有变量分配内存?还是只有进入WM_PAINT作用域时才分配内存?

方法1只有在消息为WM_PAINT时才使用内存,而方法2无论msg等于什么都会使用内存吗?


1
今天活跃使用的C语言有两种:C89/90和C99。它们在变量声明的位置方面存在很大差异。 - AnT stands with Russia
4
您说得没错,但是显示的代码符合C89或C99标准。 - Jonathan Leffler
2
如果您将函数保持在合理的复杂度范围内,那么就没有必要担心差异。 - Ben Voigt
https://dev59.com/nnI-5IYBdhLWcg3wwbZM 在很多方面非常相似,你可以在那里看到我的答案。 - Roman Nikitchenko
@Roman Nikitchenko并不完全是这样。这是关于偏好,而不一定是为了编译器的利益。当然最终会有与最佳实践相似之处,但并不完全相同。如果你只看这两种方法,它们看起来很相似,直到你看到Ben Voigt提供的第三种方法。添加一个无法被编译器内联的函数似乎不是更优的选择,但这是一个很好的解决方案,也是一个实用性很好的设计决策。 - leetNightshade
9个回答

92

变量应该尽可能在本地声明。

在函数的“顶部”声明变量总是一个灾难性的坏习惯。即使在 C89/90 语言中,变量只能在块的开头声明,也最好尽可能在最小的局部块的开头声明变量,以覆盖所需变量的生命周期。有时甚至会有意引入一个“多余”的局部块,其唯一目的是对变量声明进行“本地化”。

在 C++ 和 C99 中,可以在代码中的任何位置声明变量,答案非常明确:再次,将每个变量尽可能地声明为本地变量,并尽可能靠近首次使用它的位置。这样做的主要原因是,在大多数情况下,这将允许您在声明变量时提供有意义的初始化程序(而不是没有初始化程序或带有虚拟初始化程序)。

至于内存使用,通常的实现方式是在进入函数时立即分配同一时间存在的所有变量所需的最大空间。但是,你的声明习惯可能会影响该空间的确切大小。例如,在此代码中:

void foo() {
  int a, b, c;

  if (...) {
  }

  if (...) {
  }
}

这三个变量同时存在,通常需要为这三个变量分配空间。但在这段代码中

void foo() {
  int a;

  if (...) {
    int b;
  }

  if (...) {
    int c;
  }
}

在任何给定时刻,只有两个变量存在,这意味着典型的实现将仅为两个变量分配空间(bc将共享同一空间)。这是将变量尽可能地声明为局部变量的另一个原因。


2
在将变量声明在循环内部和尽可能本地化之间,存在一些冲突。当你在循环内部声明变量时,会影响效率。我的倾向是在循环之前声明用于大循环的变量 - 这是不好的做法吗?(假设这些变量在循环外没有用途/意义。) - flies
5
和大多数建议一样,这里也有一个没有明说的前提:“除非你在你的情况下有充分的理由做出不同的选择”。在某些情况下,效率可能足够成为将变量移出循环的理由。(尽管对于基本类型,在大多数情况下这并没有太大的区别。)你仍然可以用另一个作用域来包围变量和循环,以防止它们泄漏到其他任何地方,从而符合原始想法的意图。 - TheUndeadFish
1
@JeremyP:根据我的经验,这实际上是反对在顶部声明的一个论点:当所有声明都堆在顶部时,找到声明变得更加困难。是的,可能更容易找到整个堆,但是试图在其中找到特定声明几乎是不可能的。 - AnT stands with Russia
9
@JeremyP:我称其为灾难性糟糕,因为我能想到的缺点远远超过优点。其中一个缺点是这种声明风格鼓励变量重复使用,导致代码真正成为灾难。 - AnT stands with Russia
2
如果语言是C ++,那将取决于构造函数和operator =执行的工作量。声明和初始化调用[copy]构造函数(即使使用=符号进行初始化),而赋值(在声明之外)调用operator =。因此,在循环之外声明对象可能会更糟,因为它会创建一个将被覆盖的虚拟对象。另一方面,如果您正在使用基元、指针、引用或纯C,则无需担心,因为声明本身几乎不生成代码:真正生成代码的是=符号后面的内容。 - marcus
显示剩余6条评论

13

第一种情况下,某个东西是否分配在栈上是实现定义的。实现甚至不需要有堆栈。

通常来说,这样做并不会更,因为该操作往往是一个简单的减法(对于向下增长的堆栈)从整个局部变量区域的堆栈指针中减去一个值。

这里重要的是作用域应该尽可能地局部化。换句话说,尽可能晚地声明变量,并且只在需要时保留它们。

请注意,在这里声明与为它们分配空间处于不同的抽象级别。实际空间可能会在函数开头分配(实现级别),但只能在作用域内使用这些变量(C级别)。

信息的局部性很重要,就像封装性质一样。


7
我喜欢第三种方法:
LRESULT wpMainWindowPaint(HWND hwnd)
{
    HDC hdc;
    PAINTSTRUCT ps;

    RECT rc;
    GetClientRect(hwnd, &rc);           

    hdc = BeginPaint(hwnd, &ps);
    // drawing here
    EndPaint(hwnd, &ps);
    return 0;
}

LRESULT CALLBACK wpMainWindow(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    switch (msg)
    {
        case WM_PAINT:      return wpMainWindowPaint(hwnd);
        default:            return DefWindowProc(hwnd, msg, wparam, lparam);
    }
}

如果出于组织目的而需要拥有自己的作用域,那么它就应该拥有自己的函数。如果您担心函数调用开销,可以将其设置为内联。


5

由于编译器的工作是优化我的代码,而一小时的编译时间比我一个小时的时间要便宜得多,如果我需要滚动代码来查看变量的声明位置,我的时间就会浪费。因此,我认为我的公司希望我尽可能将所有内容保持在本地。

我甚至不是在谈论“最小的块”,而是“尽可能靠近使用它的地方”!

LRESULT CALLBACK wpMainWindow(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) 
{ 
    switch (msg) 
    { 
        case WM_PAINT: 
        { 
            RECT rc; 
            GetClientRect(hwnd, &rc);            

            { // sometimes I even create an arbitrary block 
              // to show correlated statements.
              // as a side-effect, the compiler may not need to allocate space for 
              // variables declared here...
              PAINTSTRUCT ps; 
              HDC hdc = BeginPaint(hwnd, &ps); 
              // drawing here 
              EndPaint(hwnd, &ps); 
            }
            break; 
        } 
        default:  
            return DefWindowProc(hwnd, msg, wparam, lparam); 
    } 
    return 0; 
} 

3

在变量相关的最狭窄范围内定义变量。在我看来,使用上述的方法2没有必要。

只有当变量在作用域内时才可能使用堆栈空间。正如@paxdiablo所指出的那样,如果编译器可以为它们找到空间,您的本地变量可能会在寄存器中而不是在堆栈中。


1

标准中并未详细说明内存分配,因此要得到真正的答案,您需要指定编译器和平台。但这并不影响性能。

你想要的是易读性。通常情况下,通过在尽可能小的可用范围内声明变量,并在可以立即初始化它们的合理值时进行声明,来实现易读性。变量范围越小,就越不可能以不可预测的方式与程序的其余部分交互。声明和初始化越接近,出问题的机会就越小。

更好的方式可能是:

RECT rc;
GetClientRect(hwnd, &rc);
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);

这是针对C++的。对于C语言,规则类似,只是早期版本的C要求所有变量在块的顶部声明。


1
在Java编程语言中,常见的做法是仅在方法需要时声明局部变量。
void foo(int i) {
  if (i == 1)
    return;
  Map map1 = new HashMap();
  if (i == 2)
    return;
  Map map2 = new HashMap();
}

对于C++编程语言,我也建议采用同样的做法,因为使用非平凡构造函数声明变量会涉及执行成本。如果这些变量中的一些将被使用,则将所有这些声明放在方法开头会导致不必要的成本。
void foo(int i) 
{
  if (i == 1)
    return;
  std::map<int, int> map1; // constructor is executed here
  if (i == 2)
    return;
  std::map<int, int> map2; // constructor is executed here
}

对于C语言而言,情况则有所不同。它取决于架构和编译器。对于x86和GCC,将所有声明放在函数开头并仅在需要时声明变量具有相同的性能。原因是C变量没有构造函数。这两种方法对堆栈内存分配的影响是相同的。以下是一个示例:

void foo(int i)
{
  int m[50];
  int n[50];
  switch (i) {
    case 0:
      break;
    case 1:
      break;
    default:
      break;
  }
}

void bar(int i) 
{
  int m[50];
  switch (i) {
    case 0:
      break;
    case 1:
      break;
    default:
      break;
  }
  int n[50];
}

对于这两个功能,栈操作的汇编代码如下:

pushl   %ebp
movl    %esp, %ebp
subl    $400, %esp

在 Linux 内核代码中,将所有声明放在函数开头是很常见的。


1

你无法知道堆栈分配的具体时间点。

为了提高可读性,我建议使用C99(或C ++)。这允许您在首次使用变量的地方真正声明它。

 HDC hdc = BeginPaint(hwnd, &ps);

0

没有必要用可能从未使用的变量来污染堆栈。在使用前分配您的变量。忽略RECT rc和随后调用GetClientRect,Ben Voight的方法是正确的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接