您可以做这些事情,主要是因为它们实际上并不是那么难做。
从编译器的角度来看,在另一个函数中包含一个函数声明非常容易实现。编译器需要一种机制来允许函数内部的声明来处理函数内部的其他声明(例如int x;
)。
它通常具有解析声明的通用机制。对于编写编译器的人来说,在解析另一个函数内部或外部的代码时是否调用该机制并不重要,它只是一个声明,因此当它看到足以知道存在的声明时,它调用编译器处理声明的部分。
实际上,禁止在函数中使用这些特定的声明可能会增加额外的复杂性,因为编译器随后将需要进行完全免费的检查,以查看其是否已经在函数定义内查看代码,并基于此决定是否允许或禁止使用该特定声明。宣言。
剩下的问题是嵌套函数有何不同。嵌套函数的不同之处在于它如何影响代码生成。在允许嵌套函数的语言(例如Pascal)中,您通常希望嵌套函数中的代码可以直接访问嵌套函数的变量。例如:
int foo() {
int x;
int bar() {
x = 1;
}
}
没有局部函数,访问局部变量的代码就非常简单。在典型的实现中,当执行进入函数时,将在堆栈上分配一些用于局部变量的空间块。所有局部变量都在该单个块中分配,每个变量都被简单地视为与该块开始(或结束)的偏移量。例如,让我们考虑一个类似以下的函数:
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
只需在前一个堆栈指针处查找堆栈即可访问foo
的变量。对?
错误!好吧,在某些情况下这可能是正确的,但不一定如此。特别是,bar
可以是递归的,在这种情况下,bar
可能必须查看一些几乎任意数量的级别来备份堆栈,以查找周围函数的变量。一般来说,您需要执行以下两项操作之一:将一些额外的数据放在堆栈上,以便它可以在运行时在堆栈上进行搜索以查找其周围函数的堆栈框架,或者有效地将指针传递给周围函数的堆栈框架作为嵌套函数的隐藏参数。哦,但是不一定也有一个周围的函数-如果可以嵌套函数,则可以将它们嵌套(或多或少)任意深度,因此您需要准备传递任意数量的隐藏参数。这意味着您通常最终会得到一些堆栈帧到周围函数的链接列表,
但是,这意味着访问“局部”变量可能不是一件容易的事。找到正确的堆栈框架来访问变量可能并非易事,因此(至少通常)访问周围函数的变量要比访问真正的局部变量慢。而且,当然,编译器必须生成代码以找到正确的堆栈帧,通过任意数量的堆栈帧中的任何一个访问变量,等等。
这是C通过禁止嵌套函数避免的复杂性。现在,可以肯定的是,当前的C ++编译器与1970年代的老式C编译器完全不同。对于诸如多重,虚拟继承之类的事情,C ++编译器在任何情况下都必须以相同的一般性质来处理事情(即,在这种情况下查找基类变量的位置也很重要)。从百分比的角度来看,支持嵌套函数不会给当前的C ++编译器增加太多复杂性(有些已经支持它们了,例如gcc)。
同时,它也很少增加实用性。特别是,如果你想定义的东西,行为像一个函数的功能里面,你可以使用lambda表达式。这实际上创建的是一个对象(即某个类的实例),该对象使函数调用运算符(operator()
)重载,但仍提供类似函数的功能。但是,它使从周围环境中捕获(或不捕获)数据更加明确,这使其可以使用现有机制,而不是发明一种全新的机制和使用规则。
底线:尽管一开始看起来嵌套声明很困难并且嵌套函数很琐碎,但事实恰恰相反:嵌套函数实际上比嵌套声明要复杂得多。
one
是函数定义,其他两个是声明。