实际上,你可以做到这些事情,因为它们并不是很难做到。
从编译器的角度来看,在一个函数内部有一个函数声明是相当容易实现的。编译器需要一种机制来处理函数内部的其他声明(例如 int x;
)。对于编写编译器的人来说,无论是否在解析另一个函数内的代码时调用该机制,都没有什么差别——它只是一个声明,所以当它看到足够的内容知道那里有一个声明时,就会调用处理声明的部分。
实际上,禁止在函数内部进行这些特定声明可能会增加额外的复杂性,因为编译器随后需要进行一个完全没有必要的检查,以查看是否已经正在查看函数定义内部的代码,并根据此决定是否允许或禁止此特定声明。
这就留下了嵌套函数有何不同的问题。嵌套函数之所以不同,是因为它会影响代码生成方式。在允许嵌套函数的语言中(例如 Pascal),通常预期嵌套函数中的代码可以直接访问其所嵌套的函数的变量。例如:
int foo() {
int x;
int bar() {
x = 1; // Should assign to the `x` defined in `foo`.
}
}
没有本地函数,访问本地变量的代码相当简单。在一个典型的实现中,当执行进入函数时,在堆栈上分配了一些局部变量的空间块。所有的局部变量都分配在这个单独的块中,每个变量被视为从块的开头(或结尾)的偏移量。例如,让我们考虑一个类似于以下代码的函数:
int f() {
int x;
int y;
x = 1;
y = x;
return y;
}
编译器(设想它没有优化掉额外的代码)可能会生成大约等效于以下代码的内容:
stack_pointer -= 2 * sizeof(int);
x_offset = 0;
y_offset = sizeof(int);
stack_pointer[x_offset] = 1;
stack_pointer[y_offset] = stack_pointer[x_offset];
return_location = stack_pointer[y_offset];
stack_pointer += 2 * sizeof(int);
特别地,它有一个指向局部变量块开始的位置,并且所有对局部变量的访问都是以该位置为偏移量的方式进行。
当涉及到嵌套函数时,情况就不再是这样了 - 相反,一个函数不仅可以访问自己的本地变量,还可以访问嵌套在其中的所有函数的局部变量。它不再只有一个“stack_pointer”用于计算偏移量,而是需要沿着堆栈向上查找以找到嵌套函数中的局部stack_pointers。
在一个微不足道的情况下,这并没有太大的问题 - 如果
bar
嵌套在
foo
中,则
bar
可以在堆栈中查找前一个 stack pointer 来访问
foo
的变量。对吗?
错误!嗯,有些情况下这可能是正确的,但不一定是这样。特别是如果
bar
是递归的话,那么给定的调用
bar
可能必须向上查找一些几乎任意数量的堆栈级别才能找到周围函数的变量。一般来说,你需要做以下两件事之一:要么在堆栈上放一些额外的数据,这样它就可以在运行时搜索回去找到其周围函数的堆栈帧,要么将指向周围函数的堆栈帧的指针作为嵌套函数的隐藏参数传递。哦,但也不一定只有一个周围函数 - 如果你可以嵌套函数,你可能可以任意地嵌套它们(或多或少),所以你需要准备好传递任意数量的隐藏参数。这意味着你通常会得到类似于周围函数的堆栈帧的链接列表,并且通过遍历该链接列表找到它的堆栈指针,然后从该堆栈指针访问偏移量来访问周围函数的变量。
然而,这意味着访问“本地”变量可能并不是一个微不足道的问题。找到正确的堆栈帧以访问变量可能是复杂的,因此访问周围函数的变量也(至少通常)比访问真正的局部变量慢。当然,编译器必须生成代码来找到正确的堆栈帧、通过任意数量的堆栈帧访问变量等等。
这就是 C 通过禁止嵌套函数而避免的复杂性。现在,毫无疑问,当前的 C++ 编译器与 1970 年代的 C 编译器完全不同。对于像多重虚拟继承之类的东西,C++ 编译器无论如何都必须处理这些基本的问题(例如,在这种情况下找到基类变量的位置也可能是复杂的)。在百分比上,支持嵌套函数不会给当前的 C++ 编译器增加太多的复杂性(一些编译器,如 gcc,已经支持它们)。
同时,它很少添加太多实用程序。特别地,如果你想定义一个在函数内部“表现”得像函数的东西,你可以使用 lambda 表达式。实际上创建的是一个对象(即某个类的实例),该对象重载了函数调用运算符 (
operator()
),
one
是一个函数定义,另外两个是声明。 - Some programmer dude