我知道嵌套函数不是标准 C 的一部分,但由于它们存在于 gcc 中(而且 gcc 是我唯一关心的编译器这一事实),我倾向于经常使用它们。
这是坏事吗 ?如果是这样,你能告诉我一些讨厌的例子吗?gcc 中嵌套函数的状态是什么?他们会被删除吗?
我知道嵌套函数不是标准 C 的一部分,但由于它们存在于 gcc 中(而且 gcc 是我唯一关心的编译器这一事实),我倾向于经常使用它们。
这是坏事吗 ?如果是这样,你能告诉我一些讨厌的例子吗?gcc 中嵌套函数的状态是什么?他们会被删除吗?
嵌套函数真的不会做任何非嵌套函数不能做的事情(这就是 C 和 C++ 都没有提供它们的原因)。你说你对其他编译器不感兴趣——好吧,目前这可能是真的,但谁知道未来会带来什么?我会避免使用它们以及所有其他 GCC “增强功能”。
一个小故事来说明这一点 - 我曾经在一家主要使用 DEC 盒子的英国理工学院工作 - 特别是 DEC-10 和一些 VAXen。所有的工程学院在他们的代码中都使用了 FORTRAN 的许多 DEC 扩展——他们确信我们将永远是一个 DEC 商店。然后我们用 IBM 大型机替换了 DEC-10,其 FORTRAN 编译器不支持任何扩展。我可以告诉你,那天有很多哀号和咬牙切齿。我自己的 FORTRAN 代码(一个 8080 模拟器)在几个小时内就移植到了 IBM(几乎全部用于学习如何驱动 IBM 编译器),因为我是用标准 FORTRAN-77 编写的。
有时嵌套函数可能很有用,尤其是对于围绕大量变量进行洗牌的算法。像写出来的 4 路合并排序这样的东西可能需要保留很多局部变量,并且有许多重复的代码片段使用其中的许多。将这些重复代码位作为外部辅助例程调用将需要传递大量参数和/或让辅助例程通过另一个级别的指针间接访问它们。
在这种情况下,我可以想象嵌套例程可能比其他编写代码的方式更有效地执行程序,至少如果编译器通过重新调用最外层的函数来优化存在任何递归的情况;在空间允许的情况下,内联函数在非缓存 CPU 上可能会更好,但通过单独的例程提供的更紧凑的代码可能会有所帮助。如果内部函数不能递归地调用自己或彼此,它们可以与外部函数共享一个堆栈帧,从而能够访问其变量,而不会因额外的指针取消引用而造成时间损失。
话虽如此,我会避免使用任何编译器特定的功能,除非在直接收益超过可能因必须以其他方式重写代码而导致的任何未来成本的情况下。
像大多数编程技术一样,嵌套函数应该在适当的时候且仅在适当的时候使用。
您不必强制使用此方面,但如果您愿意,嵌套函数通过直接访问其包含函数的局部变量来减少传递参数的需要。这很方便。谨慎使用“不可见”参数可以提高可读性。粗心的使用会使代码更加不透明。
避免部分或全部参数使得在别处重用嵌套函数变得更加困难,因为任何新的包含函数都必须声明这些相同的变量。重用通常是好的,但是很多功能永远不会被重用,所以这通常没关系。
由于变量的类型与其名称一起被继承,重用嵌套函数可以为您提供廉价的多态性,例如模板的有限和原始版本。
如果函数无意中访问或更改其容器的变量之一,使用嵌套函数还会引入错误的危险。想象一个包含对嵌套函数的调用的 for 循环,该嵌套函数包含一个使用相同索引但没有本地声明的 for 循环。如果我正在设计一种语言,我会包含嵌套函数,但需要“inherit x”或“inherit const x”声明,以使正在发生的事情更加明显,并避免意外的继承和修改。
还有其他几种用途,但嵌套函数做的最重要的事情可能是允许外部不可见的内部辅助函数,对 C 和 C++ 的静态非外部函数或 C++ 的私有非公共函数的扩展。有两层封装比一层要好。它还允许函数名称的局部重载,因此您不需要长名称来描述每个函数的工作类型。
当包含函数存储指向包含函数的指针以及允许多级嵌套时存在内部复杂性,但编译器编写者已经处理这些问题半个多世纪了。没有技术问题使添加到 C++ 比添加到 C 更难,但好处更少。
可移植性很重要,但 gcc 在许多环境中都可用,并且至少有一个其他编译器系列支持嵌套函数 - IBM 的 xlc 在 AIX、PowerPC 上的 Linux、BlueGene 上的 Linux、Cell 上的 Linux 和 z/OS 上可用。请参阅 http://publib.boulder.ibm.com/infocenter/comphelp/v8v101index.jsp?topic=%2Fcom.ibm.xlcpp8a.doc%2Flanguage%2Fref%2Fnested_functions.htm
嵌套函数在一些新的(例如 Python)和许多更传统的语言中可用,包括 Ada、Pascal、Fortran、PL/I、PL/IX、Algol 和 COBOL。C++ 甚至有两个受限版本——本地类中的方法可以访问其包含函数的静态(但不是自动)变量,任何类中的方法都可以访问静态类数据成员和方法。即将到来的 C++ 标准有 lamda 函数,它们是真正的匿名嵌套函数。所以编程世界有很多经验与他们赞成和反对。
嵌套函数很有用,但要小心。始终在有帮助的地方使用任何功能和工具,而不是在伤害的地方。
正如您所说,它们是一件坏事,因为它们不是 C 标准的一部分,因此许多(任何?)其他 C 编译器都没有实现。
另请记住,g++ 不实现嵌套函数,因此如果您需要获取一些代码并将其转储到 C++ 程序中,则需要删除它们。
嵌套函数可能很糟糕,因为在特定条件下,NX(不执行)安全位将被禁用。这些条件是:
使用 GCC 和嵌套函数
使用了指向嵌套函数的指针
嵌套函数从父函数访问变量
该架构提供 NX(不执行)位保护,例如 64 位 linux。
当满足上述条件时,GCC 会创建一个蹦床https://gcc.gnu.org/onlinedocs/gccint/Trampolines.html。为了支持蹦床,堆栈将被标记为可执行。见:https ://www.win.tue.nl/~aeb/linux/hh/protection.html
禁用 NX 安全位会产生几个安全问题,其中值得注意的一个是缓冲区溢出保护被禁用。具体来说,如果攻击者将一些代码放在堆栈上(例如作为用户可设置图像、数组或字符串的一部分),并且发生缓冲区溢出,那么攻击者的代码可能会被执行。
我投票删除我自己的帖子,因为它不正确。具体来说,编译器必须插入一个 trampoline 函数以利用嵌套函数,因此堆栈空间的任何节省都将丢失。
如果某些编译器大师想纠正我,请这样做!
迟到了,但我不同意接受的答案的断言
嵌套函数真的不会做任何你不能用非嵌套函数做的事情。
具体来说:
嵌套函数使您可以将词法范围的变量作为“本地”变量访问,而无需将它们推送到调用堆栈上。这在资源有限的系统上工作时非常有用,例如嵌入式系统。考虑这个人为的例子:
void do_something(my_obj *obj) {
double times2() {
return obj->value * 2.0;
}
double times4() {
return times2() * times2();
}
...
}
请注意,一旦进入 do_something(),由于嵌套函数,对 times2() 和 times4() 的调用不需要将任何参数压入堆栈,只需返回地址(智能编译器甚至会优化它们可能的)。
想象一下,如果有很多内部函数需要访问的状态。如果没有嵌套函数,所有这些状态都必须在堆栈上传递给每个函数。嵌套函数让您可以像访问局部变量一样访问状态。
我同意 Stefan 的例子,我唯一一次使用嵌套函数(然后我声明它们inline
)是在类似的场合。
我还建议你应该很少使用嵌套的内联函数,并且在你使用它们的几次中,你应该(在你的脑海中并在一些评论中)有一个摆脱它们的策略(甚至可能通过条件#ifdef __GCC__
编译来实现它)。
但是 GCC 是一个免费的(就像在语音中一样)编译器,它会有所不同......而且一些 GCC 扩展往往会成为事实上的标准,并由其他编译器实现。
我认为另一个非常有用的 GCC 扩展是计算的 goto,即label as values。在对自动机或字节码解释器进行编码时,它非常方便。
通过减少显式参数传递的数量而不引入大量全局状态,可以使用嵌套函数使程序更易于阅读和理解。
另一方面,它们不能移植到其他编译器。(注意编译器,而不是设备。gcc 没有运行的地方并不多)。
因此,如果您看到可以通过使用嵌套函数使程序更清晰的地方,您必须问自己“我是在优化可移植性还是可读性”。
我只是在探索嵌套函数的不同用途。作为 C 中“惰性评估”的一种方法。
想象这样的代码:
void vars()
{
bool b0 = code0; // do something expensive or to ugly to put into if statement
bool b1 = code1;
if (b0) do_something0();
else if (b1) do_something1();
}
相对
void funcs()
{
bool b0() { return code0; }
bool b1() { return code1; }
if (b0()) do_something0();
else if (b1()) do_something1();
}
这样您就可以清楚地了解(好吧,当您第一次看到这样的代码时可能会有些困惑),而代码仍然在且仅在需要时才执行。同时,将其转换回原始版本非常简单。
如果多次使用相同的“值”,则会出现一个问题。当所有值在编译时都已知时,GCC 能够优化为单个“调用”,但我想这对于非平凡的函数调用左右不起作用。在这种情况下,可以使用“缓存”,但这会增加不可读性。
我需要嵌套函数来允许我在对象之外使用实用程序代码。
我有照顾各种硬件设备的对象。它们是通过指针作为参数传递给成员函数的结构,就像在 c++ 中自动发生的那样。
所以我可能有
static int ThisDeviceTestBram( ThisDeviceType *pdev )
{
int read( int addr ) { return( ThisDevice->read( pdev, addr ); }
void write( int addr, int data ) ( ThisDevice->write( pdev, addr, data ); }
GenericTestBram( read, write, pdev->BramSize( pdev ) );
}
GenericTestBram 不知道也不能知道 ThisDevice,它有多个实例化。但它所需要的只是一种读写方式和一个大小。ThisDevice->read( ... ) 和 ThisDevice->Write( ... ) 需要指向 ThisDeviceType 的指针,以获取有关如何读取和写入此特定实例化的块内存 (Bram) 的信息。指针 pdev 不能具有全局范围,因为存在多个实例化,并且这些实例化可能同时运行。由于访问是通过 FPGA 接口进行的,因此传递地址并不是一个简单的问题,并且因设备而异。
GenericTestBram 代码是一个实用函数:
int GenericTestBram( int ( * read )( int addr ), void ( * write )( int addr, int data ), int size )
{
// Do the test
}
因此,测试代码只需编写一次,无需了解调用设备的结构细节。
但是,即使使用 GCC,您也不能这样做。问题是超出范围的指针,这是需要解决的问题。我知道使 f(x, ... ) 隐式知道其父级的唯一方法是传递一个值超出范围的参数:
static int f( int x )
{
static ThisType *p = NULL;
if ( x < 0 ) {
p = ( ThisType* -x );
}
else
{
return( p->field );
}
}
return( whatever );
函数 f 可以由具有指针的东西初始化,然后从任何地方调用。虽然不理想。
嵌套函数在任何严肃的编程语言中都是必备的。
没有它们,功能的实际意义是不可用的。
这称为词法作用域。