C/C++:为何编译器不允许在switch内声明变量

为什么编译器不允许在switch结构体内声明变量呢?我们可以从两个方面来解释原因。

背景

最近在整理项目代码时遇到一段编译报错的代码,例子如下:

switch (val)  
{
case VAL:
// 在switch内部声明变量
int newVal = 42;
break;
case ANOTHER_VAL:
...
break;
}

上面的代码在编译时会报以下错误(MSC):

initialization of ‘newVal’ is skipped by ‘case’ label

原因

这个错误在一开始会让人不能理解,而从编译器报错的内容来解释,就是说编译器觉得newVal的初始化可能会被跳过。我们可以从两个方面来解释编译器这么做的原因。

原因一:程序安全性

首先,我们来看下面的例子:

switch (a)
{
case 1:
int b = 0; // 'b' 在这里被初始化
break;
case 2:
b++; // 'b' 在这里被改变,但实际上其可能没有初始化
break;
case 3:
...
break;
}

我们先假设编译器会忽略switch内声明变量的错误,那么在a的值等于2的情况下,这段代码的行为是未定义的。对于变量b来说,其作用域就在紧接着switch的花括号{}内,所以在花括号内对变量b的调用都算是合法的。然而case标签的跳转功能很可能导致变量b在声明之前被调用。

case类似的语句还有goto,实际上,如果你在goto语句后声明变量,编译器会提示类似错误:

initialization of ‘a’ is skipped by ‘goto xxxx’

这也是出于对作用域以及程序安全性的考虑

如果我们一定需要在case标签后声明变量该怎么办呢?其实我们只要告诉编译器:我们在case下声明的变量绝对不可能在声明之前被调用。通过花括号{}来实现对应功能,参考下面的例子:

switch (a)
{
case 1:
{
// 变量 'b' 的作用域只在case下的花括号内
int b = 0;
break;
}
case 2:
// 对'b'的调用是不允许的,因为在作用域内,并没有声明变量'b'
// b++;
break;
case 3:
...
break;
}

上面的代码是可以通过编译器检查的,我们使用花括号告诉编译器变量b的作用范围仅仅在当前case标签下,这样就可以避免上面提到的安全性问题,编译器自然不会报错。

原因二:编译问题

学习过C/C++的人多多少少都会知道程序实际上运行在栈(stack)上的,函数的调用都会涉及对栈的操作:压入(Push)和弹出(Pop)。而每次函数调用时机器都会压入一定数量的数据,同时在函数返回时弹出对应的数据。而具体压入和弹出的数据量是编译器在编译时决定的。假如编译器在函数中发现int a;的语句,那么它就知道函数需要压入int对应的数据大小(也就是4字节)。

由于一些跳转标签(casegoto)和分支逻辑(ifelse)的存在,编译器无法在编译时确定程序的实际运行路径的。如果在跳转语句后出现了变量的声明,编译器无法确定这个变量是否真的会被创建。因此也就无法确定需要压栈的大小。

类似的问题都可以通过花括号来解决,就像跟在ifelse语句后面的花括号一样,虽然ifelse也有跳转的意思,但是其后跟着的花括号表明这是一个独立的代码块,程序执行到这里时,会重新压入对应的数据,并在离开花括号时弹出对应的数据。因此在花括号内的声明是不受跳转语句影响的。

此文有用? 求鼓励!

显示 Gitment 评论