(我知道C语言规范没有指定分配是否发生在堆栈,堆或其他某种机制上-但出于这个问题,我们假设所有自动变量都存在于调用堆栈中, alloca(size_t)
始终在堆栈上分配(与malloca
不同),malloc
/ calloc
等在堆栈上分配)。
在C语言中,如果您有一个函数来初始化内存的某些区域,则需要预先分配该内存,然后将一个指针(如果需要,带有长度)传递给初始化程序:
void plumbusProgram() {
const size_t plumbusSize = 123;
void* plumbusMemory = alloca( plumbusSize );
struct plumbusVla* = initializePlumbus( plumbusMemory, plumbusSize );
doStuffWithPlumbus( plumbusVla );
// `plumbusMemory` ceases to exist as allocated memory when this function returns.
}
如果要分配的内存大小是可变的,则它将负责了解分配大小的责任交给拥有该分配的函数,而不是由初始化内存的函数来负责。我从不喜欢这个要求,因为它破坏了SRP。
void plumbusProgram( enum plumbusType type ) {
size_t plumbusMemorySize = getPlumbusMemorySize( type ); // <-- ewww
void* plumbusMemory = alloca( plumbusMemorySize );
struct plumbusVla* = initializePlumbus( plumbusMemory, plumbusSize );
doStuffWithPlumbus( plumbusVla );
// `plumbusMemory` ceases to exist as allocated memory when this function returns.
}
那为什么initializePlumbus
既不能分配又要初始化plumbusMemory
?当前,在可移植的C语言中(当然没有UB),initializePlumbus
函数需要在堆上分配并返回指向堆的指针-这意味着现在调用方需要显式管理分配的堆的生存期通过在plumbusMemory
返回之前调用free
plumbusProgram
,包括在每种可能的错误情况下(或在C ++中,使用智能指针)。
我意识到initializePlumbus
仍然可以在堆栈上分配,只要它可以返回而不会弹出堆栈-但是每种方法都有问题。
首先,这是我希望程序的外观:
void plumbusProgram( enum plumbusType type ) {
struct plumbusVla* = initializePlumbus( type ); // allocates the memory for `plumbusVla` inside `plumbusProgram`'s stack frame, so the memory is automatically deallocated when `plumbusProgram` returns.
doStuffWithPlumbus( plumbusVla );
// `plumbusVla`'s memory ceases to exist when this function returns.
}
以下是我考虑过的几种方法:
void plumbusProgram( enum plumbusType type ) {
size_t plumbusMemorySize;
struct plumbusVla* = initializePlumbus( type, &plumbusMemorySize );
// Artifically raise the stack pointer to beyond plumbusMemory so the next function-call (and anything else added to the stack) won't overwrite plumbusMemory:
alloca( plumbusMemorySize );
// It's now safe to call other functions:
doStuffWithPlumbus( plumbusVla );
// `plumbusVla`'s memory ceases to exist when this function returns.
}
struct plumbusVla* initializePlumbus( enum plumbusType type, size_t* plumbusMemorySize ) {
*plumbusMemorySize = getPlumbusMemorySize( type );
void* plumbusMemory = alloca( plumbusMemorySize );
// (do rest of initialization here)
return (struct plumbusVla*)plumbusMemory;
}
此处的缺点是顶级plumbusProgram
函数仍然具有plumbusMemorySize
的知识,并且不得不滥用alloca
来移动堆栈指针。
setjmp
/ longjmp
UB:在堆上分配后,使用setjmp
/ longjmp
从initializePlumbus
内部跳回到plumbusProgram
并没有帮助,因为在调用longjmp
时,原始的堆栈指针-以及关于plumbusMemory
大小的知识将丢失。
例程接收控件可以访问的所有变量(寄存器变量除外)的值包含它们在调用
longjmp
时所具有的值。
因此,如果plumbusMemory
的大小通过指向本地的指针传递回plumbusProgram
,则堆栈状态仍将重置,因此值将丢失。存储plumbusMemory
大小的唯一方法是使用非堆栈变量,例如堆变量(这违背了本练习的目的),单个全局变量(这意味着您不能拥有更多的变量)。大于1个已分配堆栈的对象)或一个全局固定大小的堆栈(仍限制了已分配堆栈的对象的数量)。
在分配之后(在initializePlumbus
内部保存堆栈状态不起作用,因为在longjmp
返回后从plumbusProgram
调用initializePlumbus
只会导致循环,因为PC也将从initializePlumbus
内部恢复。
setcontext
/ getcontext
这些(不幸的是现在已经过时的)POSIX函数可能有用,但是我对它们不太熟悉。
这有点作弊-并使代码特定于单个ISA-但它完美地解决了问题-像这样:
void plumbusProgram( enum plumbusType type ) {
struct plumbusVla* = initializePlumbus( plumbusMemory, plumbusSize );
}
__declspec( naked ) size_t initializePlumbus( enum plumbusType type ) {
// Need to provide both prolog and epilog, even though I only want a custom epilog - so here's the __stdcall prolog:
__asm {
push ebp
mov ebp, esp
sub esp, __LOCAL_SIZE
}
*plumbusMemorySize = getPlumbusMemorySize( type );
void* plumbusMemory = alloca( plumbusMemorySize );
// (do rest of initialization here)
// Custom epilog: Restores ebp but not esp:
__asm {
pop ebp
jmp ebp ; return to caller without popping stack (instead of `ret`)
}
}
...但是显然这是一个黑客。
令我感到惊讶的是,C(尤其是POSIX C)似乎没有任何内置方法来允许将 delegation 分配给另一个函数。 >