The difference between the block and function scopes in C - c

What is the difference between the block and function scopes in C99 in terms of what happens on stack when a function / block is entered and left?

In theory, a compiler could generate code to allocate a stack frame on entry to any block that contains local variables. In such a case, there wouldn't be much difference at all.
In practice, most compilers compute the maximum size of local variables that could be used by any path through a function, then allocate that size of stack frame on entry. Variables in any block inside the function are simply different offsets from the stack pointer. Note that in such a case, two (or more) blocks may use the same addresses. For example, with source code like this:
void f(int x) {
if (x) {
long y;
}
else {
float z;
}
}
...chances are quite good that y and z will end up at the same address.

Like this:
void foo(int n) // <-- beginning of function scope
{ // <-- beginning of function body scope
int x = n;
for (;;)
{ // <-- beginning of block scope
int q = n;
x *= q;
} // <-- end of block scope
foo(x);
{ // <-- another block scope
int w = x;
}
} // <-- end of function body scope
// and of function scope
Nothing "happens" when a scope ends, but a variable only lives inside the scope where it is declared (with some arcane exceptions). It is up to the implementation to reuse the space of variables of previous, nested scopes that have ended.

The only thing an implementation is required to do when control enters either a function or a block scope is to behave as if new instances have been created of all data objects directly in that scope with "automatic storage duration." Behave as if means it can do something different as long as the program being compiled can't tell the difference (or could only tell the difference by doing something whose behavior is undefined). For instance, if a variable is declared at function scope but only used within one subblock, the compiler can collapse its live range to that subblock, and probably will, because this makes register allocation easier.
An implementation is not required to do anything when control exits a function or block scope. The lifetimes of all automatic-storage-duration objects directly in that scope end, but no program can tell that this has happened without triggering undefined behavior.
There is no requirement for a C implementation to have a stack, and a stack is not the only way to implement the above requirements. See for instance "Cheney on the M.T.A." and c2:SpaghettiStack.
C implementations that do have a stack will normally try to avoid adjusting the stack pointer in the middle of a function, for reasons too complicated to go into here. This can mean that a value with block scope survives on the stack longer than its declared lifetime, but it's still undefined behavior to access it. The compiler is allowed to recycle storage for values that are no longer in scope, but it is also allowed to recycle storage for values that are still in scope but will not be accessed anymore ("dead" in compiler jargon). Historically compilers have been much more aggressive about doing that for values in registers than for values in stack slots, but again, that's a distinction that doesn't necessarily exist on your implementation.

Related

C - Why variables created in a loop have the same memory address?

Just a simple example of my problem:
while(condition){
int number = 0;
printf("%p", &number);
}
That variable will always be in the same memory address. Why?
And what's the real difference between declaring it inside or outside the loop then?
Would I need to malloc the variable every iteration to get different addresses?
That variable will always be in the same memory address. Why?
It's not required to, but your code is so simple that it probably will be across all platforms. Specifically, because it's stored on the stack, it's always in the same place relative to your stack pointer. Keep in mind you're not allocating memory here (no new or malloc), you're just naming existing (stack-relative) memory.
And what's the real difference between declaring it inside or outside the loop then?
In this case, scope. The variable doesn't exist outside the braces where it lives in. Also outside of the braces, another variable can take its place if it fits in memory and the compiler chooses to do this.
Would I need to malloc the variable every iteration to get different addresses?
Yes, but I have yet to see a good use of malloc to allocate space for an int that a simple stack variable or a standard collection wouldn't do better.
That variable will always be in the same memory address. Why?
The compiler decides where the variable should be, given the operating system constraints, it's much more efficient to maintain the variable at the same address than having it relocated at every iteration, but this could, theoretically, happen.
You can't rely on it being in the same address every time.
And what's the real difference between declaring it inside or outside the loop then?
The difference is lifetime of the variable, if declared within the loop it will only exist inside the loop, you can't access it after the loop ends.
When execution of the block ends the lifetime of the object ends and it can no longer be accessed.
Would I need to malloc the variable every iteration to get different addresses?
malloc is an expensive operation, it does not make much sense to malloc the variable at every iteration, that said, again, the compiler decides where the memory for it is allocated, it may very well be at the same address or not.
Once again you can't rely on the variable location in the previous iteration to assert where it will be on the next one.
There is a difference in the the variables are stored, allocated variables will be on the heap, as opposed to the stack like in the previous case.
It is being put into the same memory address to save memory.
The only real difference between declaring it within and without the loop is that the variable will no longer be within scope outside the loop if it was declared within the loop.
You would have to use malloc to get a different address each time. Also, you would have to leave the frees until after all the mallocs to get this guarantee.
That variable will always be in the same memory address. Why?
The object that number designates has auto storage duration and only exists for the lifetime of the loop body, so logically speaking a new instance is created and destroyed on each loop iteration.
Practically speaking, it's easier to just re-use the same memory location for each loop iteration, which is what most (if not all) C compilers do. It's just not guaranteed to retain its last value from one iteration to the next (especially if you initialize it each iteration).
And what's the real difference between declaring it inside or outside the loop then?
The lifetime of the object (the period of program execution where storage is guaranteed to be reserved for it) changes from the body of the loop to the body of the function. The scope of the identifier (the region of program text where the identifier is visible) changes from the body of the loop to the body of the entire function.
Again, practically speaking, most compilers will allocate stack space for auto objects that are in blocks at function entry - for example, given the code
void foo( void )
{
int bar;
while ( bar = 0; bar < 10; bar++ )
{
int bletch = 2 * bar;
...
}
}
most compilers will generate instructions to reserve stack space for both bar and bletch at function entry, rather than waiting until loop entry to reserve space for bletch. It's just easier to set the stack pointer once and get it over with. Storage is guaranteed to be reserved for bletch over the lifetime of the loop body, but there's nothing in the language definition that says you can't reserve it before then.
However, if you have a situation like this:
void foo( void )
{
int bar;
while ( bar = 0; bar < 10; bar++ )
{
if ( bar % 2 == 0 ) // bar is even
{
int bletch = 2 * bar;
...
}
else
{
int blurga = 3 * bar + 1;
...
}
}
bletch and blurga cannot exist at the same time, so the compiler may only allocate space for one additional int object, and that same space will be used for either bletch or blurga depending on the value of bar.
There are compilers that, despite you declaring the variable in the inner loop, just allocate them at the entry to the function block.
Modern compilers tend to allocate all memory for local variables in a single shot at function entry, so that only represents a single stack pointer move, against several push pop instructions to get the same result.
Despite of that, there's another issue you have not considered. The variable in the inner loop is not visible outside the loop, and the memory used by it can be used for a different variable outside. You know that the memory address is always the same... but you don't know when you are out of scope if any of the other variables you use for a different thing are given the same address by the compiler (that's perfectly legal, as your variable is automatic, and so, it ceases to exist as soon as you get out of the block (the pair of curly brackets you put around the loop)

Memory allocation of functions and variables in C

Depended of the version of the C compiler and compiler flags it is possible to initialize variables on any place in your functions (As far as I am aware).
I'm used to put it all the variables at the top of the function, but the discussion started about the memory use of the variables if defined in any other place in the function.
Below I have written 2 short examples, and I wondered if anyone could explain me (or verify) how the memory gets allocated.
Example 1: Variable y is defined after a possible return statement, there is a possibility this variable won't be used for that reason, as far as I'm aware this doesn't matter and the code (memory allocation) would be the same if the variable was placed at the top of the function. Is this correct?
Example 2: Variable x is initialized in a loop, meaning that the scope of this variable is only within this loop, but what about the memory use of this variable? Would it be any different if placed on the top of the functions? Or just initialized on the stack at the function call?
Edit: To conclude a main question:
Does reducing the scope of the variable or change the location of the first use (so anywhere else instead of top) have any effects on the memory use?
Code example 1
static void Function(void){
uint8_t x = 0;
//code changing x
if(x == 2)
{
return;
}
uint8_t y = 0;
//more code changing y
}
Code example 2
static void LoopFunction(void){
uint8_t i = 0;
for(i =0; i < 100; i ++)
{
uint8_t x = i;
// do some calculations
uartTxLine("%d", x);
}
//more code
}
I'm used to put it all the variables at the top of the function
This used to be required in the older versions of C, but modern compilers dropped that requirement. As long as they know the type of the variable at the point of its first use, the compilers have all the information they need.
I wondered if anyone could explain me how the memory gets allocated.
The compiler decides how to allocate memory in the automatic storage area. Implementations are not limited to the approach that gives each variable you declare a separate location. They are allowed to reuse locations of variables that go out of scope, and also of variables no longer used after a certain point.
In your first example, variable y is allowed to use the space formerly occupied by variable x, because the first point of use of y is after the last point of use of x.
In your second example the space used for x inside the loop can be reused for other variables that you may declare in the // more code area.
Basically, the story goes like this. When calling a function in raw assembler, it is custom to store everything used by the function on the stack upon entering the function, and clean it up upon leaving. Certain CPUs and ABIs may have a calling convention which involves automatic stacking of parameters.
Likely because of this, C and many other old languages had the requirement that all variables must be declared at the top of the function (or on top of the scope), so that the { } reflect push/pop on the stack.
Somewhere around the 80s/90s, compilers started to optimize such code efficiently, as in they would only allocate room for a local variable at the point where it was first used, and de-allocate it when there was no further use for it. Regardless of where that variable was declared - it didn't matter for the optimizing compiler.
Around the same time, C++ lifted the variable declaration restrictions that C had, and allowed variables to be declared anywhere. However, C did not actually fix this before the year 1999 with the updated C99 standard. In modern C you can declare variables everywhere.
So there is absolutely no performance difference between your two examples, unless you are using an incredibly ancient compiler. It is however considered good programming practice to narrow the scope of a variable as much as possible - though it shouldn't be done at the expense of readability.
Although it is only a matter of style, I would personally prefer to write your function like this:
(note that you are using the wrong printf format specifier for uint8_t)
#include <inttypes.h>
static void LoopFunction (void)
{
for(uint8_t i=0; i < 100; i++)
{
uint8_t x = i;
// do some calculations
uartTxLine("%" PRIu8, x);
}
//more code
}
Old C allowed only to declare (and initialize) variables at the top of a block. You where allowed to init a new block (a pair of { and } characters) anywhere inside a block, so you had then the possibility of declaring variables next to the code using them:
... /* inside a block */
{ int x = 3;
/* use x */
} /* x is not adressabel past this point */
And you where permitted to do this in switch statements, if statements and while and do statements (everywhere where you can init a new block)
Now, you are permitted to declare a variable anywhere where a statement is allowed, and the scope of that variable goes from the point of declaration to the end of the inner nested block you have declared it into.
Compilers decide when they allocate storage for local variables so, you can all of them allocated when you create a stack frame (this is the gcc way, as it allocates local variables only once) or when you enter in the block of definition (for example, Microsoft C does this way) Allocating space at runtime is something that requires advancing the stack pointer at runtime, so if you do this only once per stack frame you are saving cpu cycles (but wasting memory locations). The important thing here is that you are not allowed to refer to a variable location outside of its scoping definition, so if you try to do, you'll get undefined behaviour. I discovered an old bug for a long time running over internet, because nobody take the time to compile that program using Microsoft-C compiler (which failed in a core dump) instead of the commmon use of compiling it with GCC. The code was using a local variable defined in an inner scope (the then part of an if statement) by reference in some other part of the code (as everything was on main function, the stack frame was present all the time) Microsoft-C just reallocated the space on exiting the if statement, but GCC waited to do it until main finished. The bug solved by just adding a static modifier to the variable declaration (making it global) and no more refactoring was neccesary.
int main()
{
struct bla_bla *pointer_to_x;
...
if (something) {
struct bla_bla x;
...
pointer_to_x = &x;
}
/* x does not exist (but it did in gcc) */
do_something_to_bla_bla(pointer_to_x); /* wrong, x doesn't exist */
} /* main */
when changed to:
int main()
{
struct bla_bla *pointer_to_x;
...
if (something) {
static struct bla_bla x; /* now global ---even if scoped */
...
pointer_to_x = &x;
}
/* x is not visible, but exists, so pointer_to_x continues to be valid */
do_something_to_bla_bla(pointer_to_x); /* correct now */
} /* main */

If I define an array in if statement then does memory get allocated?

If I define an array in if statement then does memory gets allocated during compile time eg.
if(1)
{
int a[1000];
}
else
{
float b[1000];
}
Then a memory of 2 * 1000 for ints + 4 * 1000 for floats get allocated?
It is reserved on the stack at run-time (assuming a non-trivial condition - in your case, the compiler would just exclude the else part). That means it only exists inside the scope block (between the {}).
In your example, only the memory for the ints gets allocated on the stack (1000 * sizeof(int)).
As you can guess, this is happening at run time. The generated code has instructions to allocate the space on the stack when the corresponding block of code is entered.
Keep in mind that this is happening because of the semantics of the language. The block structure introduces a new scope, and any automatic variables allocated in that scope have a lifetime that lasts as long as the scope does. In C, this is implemented by allocating it on the stack, which collapses as the scope disappears.
Just to drive home the point, note that the allocation would be different had the variables been of different nature.
if(1)
{
static int a[1000];
}
else
{
static float b[1000];
}
In this case, space is allocated for both the ints and the floats. The lifetime of these variables is the program. But the visibility is within the block scope they are allocated in.
Scope
Variables declared inside the scope of a pair of { } are on the stack. This applies to variables declared at the beginning of a function or in any pair of { } within the function.
int myfunc()
{
int i = 0; // On the stack, scoped: myfunc
printf("%i\n");
if (1)
{
int j = 1; // On the stack, scope: this if statement
printf("%i %i\n",i,j);
}
printf("%i %i\n",i,j); // Won't work, no j
}
These days the scope of the variables is limited to the surrounding { }. I recall that some older Microsoft compilers didn't limit the scope, and that in the example above the final printf() would compile.
So Where is it in memory?
The memory of i and j is merely reserved on the stack. This is not the same as memory allocation done with malloc(). That is important, because calling malloc() is very slow in comparison. Also with memory dynamically allocated using malloc() you have to call free().
In effect the compiler knows ahead of time what space is needed for a function's variables and will generate code that refers to memory relative to whatever the stack pointer is when myfunc() is called. So long as the stack is big enough (2MBytes normally, depends on the OS), all is good.
Stack overflow occurs in the situation where myfunc() is called with the stack pointer already close to the end of the stack (i.e. myfunc() is called by a function which in turn had been called by another which it self was called by yet another, etc. Each layer of nested calls to functions moves the stack pointer on a bit more, and is only moved back when functions return).
If the space between the stack pointer and the end of the stack isn't big enough to hold all the variables that are declared in myfunc(), the code for myfunc() will simply try to use locations beyond the end of the stack. That is almost always a bad thing, and exactly how bad and how hard it is to notice that something has gone wrong depends on the operating system. On small embedded micro controllers it can be a nightmare as it usually means some other part of the program's data (eg global variables) get silently overwritten, and it can be very hard to debug. On bigger systems (Linux, Windows) the OS will tell you what's happened, or will merely make the stack bigger.
Runtime Efficiency Considerations
In the example above I'm assigning values to i and j. This does actually take up a small amount of runtime. j is assigned 1 only after evaluation of the if statement and subsequent branch into where j is declared.
Say for example the if statement hadn't evaluated as true; in that case j is never assigned 1. If j was declared at the start of myfunc() then it would always get assigned the value of 1 regardless of whether the if statement was true - a minor waste of time. But consider a less trivial example where a large array is declared an initialised; that would take more execution time.
int myfunc()
{
int i = 0; // On the stack, scoped: myfunc
int k[10000] = {0} // On the stack, scoped: myfunc. A complete waste of time
// when the if statement evaluates to false.
printf("%i\n");
if (0)
{
int j = 1; // On the stack, scope: this if statement
// It would be better to move the declaration of k to here
// so that it is initialised only when the if evaluates to true.
printf("%i %i %i\n",i,j,k[500]);
}
printf("%i %i\n",i,j); // Won't work, no j
}
Placing the declaration of k at the top of myfunc() means that a loop 10,000 long is executed to initialise k every time myfunc() is called. However it never gets used, so that loop is a complete waste of time.
Of course, in these trivial examples compilers will optimise out the unnecessary code, etc. In real code where the compiler cannot predict ahead of time what the execution flow will be then things are left in place.
Memory for the array in the if block will be allocated on stack at run time. else part will be optimized (removed) by the compiler. For more on where the variables will be allocated memory, see Segmentation Fault when writing to a string
As DCoder & paddy corrected me, the memory will be calculated at compile time but allocated at run-time in stack memory segment, but with the scope & lifetime of the block in which the array is defined. The size of memory allocated depends on size of int & float in your system. Read this for an overview on C memory map

Ampersand bug and lifetime in c

As we know, local variables have local scope and lifetime. Consider the following code:
int* abc()
{
int m;
return(&m);
}
void main()
{
int* p=abc();
*p=32;
}
This gives me a warning that a function returns the address of a local variable.
I see this as justification:
Local veriable m is deallocated once abc() completes. So we are dereferencing an invalid memory location in the main function.
However, consider the following code:
int* abc()
{
int m;
return(&m);
int p=9;
}
void main()
{
int* p=abc();
*p=32;
}
Here I am getting the same warning. But I guess that m will still retain its lifetime when returning. What is happening? Please explain the error. Is my justification wrong?
First, notice that int p=9; will never be reached, so your two versions are functionally identical. The program will allocate memory for m and return the address of that memory; any code below the return statement is unreacheable.
Second, the local variable m is not actually de-allocated after the function returns. Rather, the program considers the memory free space. That space might be used for another purpose, or it might stay unused and forever hold its old value. Because you have no guarantee about what happens to the memory once the abc() function exits, you should not attempt to access or modify it in any way.
As soon as return keyword is encountered, control passes back to the caller and the called function goes out of scope. Hence, all local variables are popped off the stack. So the last statement in your second example is inconsequential and the warning is justified
Logically, m no longer exists when you return from the function, and any reference to it is invalid once the function exits.
Physically, the picture is a bit more complicated. The memory cells that m occupied are certainly still there, and if you access those cells before anything else has a chance to write to them, they'll contain the value that was written to them in the function, so under the right circumstances it's possible for you to read what was stored in m through p after abc has returned. Do not rely on this behavior being repeatable; it is a coding error.
From the language standard (C99):
6.2.4 Storage durations of objects
...
2 The lifetime of an object is the portion of program execution during which storage is
guaranteed to be reserved for it. An object exists, has a constant address,25) and retains
its last-stored value throughout its lifetime.26) If an object is referred to outside of its
lifetime, the behavior is undefined. The value of a pointer becomes indeterminate when
the object it points to reaches the end of its lifetime.
25) The term ‘‘constant address’’ means that two pointers to the object constructed at possibly different
times will compare equal. The address may be different during two different executions of the same
program.
26) In the case of a volatile object, the last store need not be explicit in the program.
Emphasis mine. Basically, you're doing something that the language definition explicitly calls out as undefined behavior, meaning the compiler is free to handle that situation any way it wants to. It can issue a diagnostic (which your compiler is doing), it can translate the code without issuing a diagnostic, it can halt translation at that point, etc.
The only way you can make m still valid memory (keeping the maximum resemblance with your code) when you exit the function, is to prepend it with the static keyword
int* abc()
{
static int m;
m = 42;
return &m;
}
Anything after a return is a "dead branch" that won't be ever executed.
int m should be locally visible. You should create it as int* m and return it directly.

In C, do braces act as a stack frame?

If I create a variable within a new set of curly braces, is that variable popped off the stack on the closing brace, or does it hang out until the end of the function? For example:
void foo() {
int c[100];
{
int d[200];
}
//code that takes a while
return;
}
Will d be taking up memory during the code that takes a while section?
No, braces do not act as a stack frame. In C, braces only denote a naming scope, but nothing gets destroyed nor is anything popped off the stack when control passes out of it.
As a programmer writing code, you can often think of it as if it is a stack frame. The identifiers declared within the braces are only accessible within the braces, so from a programmer's point of view, it is like they are pushed onto the stack as they are declared and then popped when the scope is exited. However, compilers don't have to generate code that pushes/pops anything on entry/exit (and generally, they don't).
Also note that local variables may not use any stack space at all: they could be held in CPU registers or in some other auxiliary storage location, or be optimized away entirely.
So, the d array, in theory, could consume memory for the entire function. However, the compiler may optimize it away, or share its memory with other local variables whose usage lifetimes do not overlap.
The time during which the variable is actually taking up memory is obviously compiler-dependent (and many compilers don't adjust the stack pointer when inner blocks are entered and exited within functions).
However, a closely related but possibly more interesting question is whether the program is allowed to access that inner object outside the inner scope (but within the containing function), ie:
void foo() {
int c[100];
int *p;
{
int d[200];
p = d;
}
/* Can I access p[0] here? */
return;
}
(In other words: is the compiler allowed to deallocate d, even if in practice most don't?).
The answer is that the compiler is allowed to deallocate d, and accessing p[0] where the comment indicates is undefined behaviour (the program is not allowed to access the inner object outside of the inner scope). The relevant part of the C standard is 6.2.4p5:
For such an object [one that has
automatic storage duration] that does
not have a variable length array type,
its lifetime extends from entry into the block with which it is associated
until execution of that block ends in
any way. (Entering an enclosed block
or calling a function suspends, but
does not end, execution of the current
block.) If the block is entered
recursively, a new instance of the
object is created each time. The
initial value of the object is
indeterminate. If an initialization is
specified for the object, it is
performed each time the declaration is
reached in the execution of the block;
otherwise, the value becomes
indeterminate each time the
declaration is reached.
Your question is not clear enough to be answered unambiguously.
On the one hand, compilers don't normally do any local memory allocation-deallocation for nested block scopes. The local memory is normally allocated only once at function entry and released at function exit.
On the other hand, when the lifetime of a local object ends, the memory occupied by that object can be reused for another local object later. For example, in this code
void foo()
{
{
int d[100];
}
{
double e[20];
}
}
both arrays will usually occupy the same memory area, meaning that the total amount of the local storage needed by function foo is whatever is necessary for the largest of two arrays, not for both of them at the same time.
Whether the latter qualifies as d continuing to occupy memory till the end of function in the context of your question is for you to decide.
It's implementation dependent. I wrote a short program to test what gcc 4.3.4 does, and it allocates all of the stack space at once at the start of the function. You can examine the assembly that gcc produces using the -S flag.
No, d[] will not be on the stack for the remainder of routine. But alloca() is different.
Edit: Kristopher Johnson (and simon and Daniel) are right, and my initial response was wrong. With gcc 4.3.4.on CYGWIN, the code:
void foo(int[]);
void bar(void);
void foobar(int);
void foobar(int flag) {
if (flag) {
int big[100000000];
foo(big);
}
bar();
}
gives:
_foobar:
pushl %ebp
movl %esp, %ebp
movl $400000008, %eax
call __alloca
cmpl $0, 8(%ebp)
je L2
leal -400000000(%ebp), %eax
movl %eax, (%esp)
call _foo
L2:
call _bar
leave
ret
Live and learn! And a quick test seems to show that AndreyT is also correct about multiple allocations.
Added much later: The above test shows the gcc documentation is not quite right. For years it has said (emphasis added):
"The space for a variable-length array is deallocated as soon as the array name's scope ends."
They might. They might not. The answer I think you really need is: Don't ever assume anything. Modern compilers do all kinds of architecture and implementation-specific magic. Write your code simply and legibly to humans and let the compiler do the good stuff. If you try to code around the compiler you're asking for trouble - and the trouble you usually get in these situations is usually horribly subtle and difficult to diagnose.
Your variable d is typically not popped off the stack. Curly braces do not denote a stack frame. Otherwise, you would not be able to do something like this:
char var = getch();
{
char next_var = var + 1;
use_variable(next_char);
}
If curly braces caused a true stack push/pop (like a function call would), then the above code would not compile because the code inside the braces would not be able to access the variable var that lives outside the braces (just like a sub-function cannot directly access variables in the calling function). We know that this is not the case.
Curly braces are simply used for scoping. The compiler will treat any access to the "inner" variable from outside the enclosing braces as invalid, and it may re-use that memory for something else (this is implementation-dependent). However, it may not be popped off of the stack until the enclosing function returns.
Update: Here's what the C spec has to say. Regarding objects with automatic storage duration (section 6.4.2):
For an object that does not have a variable length array type, its
lifetime extends from entry into the block with which it is associated
until execution of that block ends in anyway.
The same section defines the term "lifetime" as (emphasis mine):
The lifetime of an object is the portion of program execution during
which storage is guaranteed to be reserved for it. An object exists,
has a constant address, and retains its last-stored value throughout
its lifetime. If an object is referred to outside of its lifetime, the
behavior is undefined.
The key word here is, of course, 'guaranteed'. Once you leave the scope of the inner set of braces, the array's lifetime is over. Storage may or may not still be allocated for it (your compiler might re-use the space for something else), but any attempts to access the array invoke undefined behavior and bring about unpredictable results.
The C spec has no notion of stack frames. It speaks only to how the resulting program will behave, and leaves the implementation details to the compiler (after all, the implementation would look quite different on a stackless CPU than it would on a CPU with a hardware stack). There is nothing in the C spec that mandates where a stack frame will or will not end. The only real way to know is to compile the code on your particular compiler/platform and examine the resulting assembly. Your compiler's current set of optimization options will likely play a role in this as well.
If you want to ensure that the array d is no longer eating up memory while your code is running, you can either convert the code in curly braces into a separate function or explicitly malloc and free the memory instead of using automatic storage.
I believe that it does go out of scope, but is not pop-ed off the stack until the function returns. So it will still be taking up memory on the stack until the function is completed, but not accessible downstream of the first closing curly brace.
There has already been given much information on the standard indicating that it is indeed implementation specific.
So, one experiment might be of interest. If we try the following code:
#include <stdio.h>
int main() {
int* x;
int* y;
{
int a;
x = &a;
printf("%p\n", (void*) x);
}
{
int b;
y = &b;
printf("%p\n", (void*) y);
}
}
Using gcc we obtain here two times the same address: Coliro
But if we try the following code:
#include <stdio.h>
int main() {
int* x;
int* y;
{
int a;
x = &a;
}
{
int b;
y = &b;
}
printf("%p\n", (void*) x);
printf("%p\n", (void*) y);
}
Using gcc we obtain here two different addresses: Coliro
So, you can't be really sure what is going on.

Resources