C / FUNCTIONS
Scope, lifetime and shadowing local variables
Trace which declaration a name refers to inside nested blocks, say exactly when a local's storage exists, and recognize shadowing bugs.
What you will learn
- Resolve a name by searching outward from the innermost enclosing block
- State when an automatic variable's storage is created and destroyed
- Tell a shadowing declaration apart from an assignment to the outer variable
- Avoid dangling pointers by checking an object outlives the block that declared it
Understanding Scope, lifetime and shadowing local variables
A block, meaning any { ... }, introduces a scope, and a declaration inside it is visible from the end of its own declarator down to the block's closing brace. Function parameters behave as if they were declared at the top of the function body, so the body can hide them but nothing outside the function can see them. Scope is purely a compile-time question: it tells the compiler which declaration a particular occurrence of a name refers to, and that decision is fixed before the program ever runs.
Lifetime is the separate, run-time question of when storage exists. A local without static has automatic storage: the object comes into existence when control enters its block and stops existing when control leaves the block by any route. Each entry produces a fresh object, which is why a variable declared inside a loop body is back at its initializer on every iteration, and why a local with no initializer holds an indeterminate value, since nothing cleared that storage for you. Keeping a pointer to such an object past the closing brace leaves you holding an address to storage the program is free to reuse.
Shadowing happens when an inner declaration reuses a name an enclosing block already uses. The inner declaration wins for the rest of that block, because name lookup starts in the innermost block and stops at the first declaration it finds, so the outer object is still alive but temporarily has no name. That is why assigning to a shadow changes nothing outside the block, and why an int accidentally left in front of an assignment inside an if turns a real update into a write to a variable that dies one line later.
<stdio.h>
int main(void)
{
int n = 1;
printf("outer n = %d\n", n);
{ /* a new block, a new scope */
int n = 2; /* shadows the outer n from here down */
printf(" inner n = %d\n", n);
n = 20;
printf(" inner n = %d after assignment\n", n);
} /* the inner n's lifetime ends here */
printf("outer n = %d, unchanged\n", n);
for (int i = 0; i < 3; i++) {
int seen = 0; /* a fresh object on every iteration */
seen++;
printf(" i = %d, seen = %d\n", i, seen);
}
printf("done\n");
return 0;
}
Scope is a compile-time rule about which declaration a name binds to, lifetime is a run-time fact about when that object's storage exists, and shadowing only affects the first.
Worked examples
Where a name starts being visible
Shows that a declaration's scope begins at its own declarator, so code above it in the same block still sees the outer variable.
<stdio.h>
int main(void)
{
int size = 3;
{
int copy = size; /* no inner size exists yet: outer one is used */
int size = copy * 2; /* the inner size begins here */
printf("inner: copy = %d, size = %d\n", copy, size);
}
printf("outer: size = %d\n", size);
return 0;
}
Example explained
Line 1int copy = size; is evaluated before any inner size is declared, so the name resolves to the outer variable and copy becomes 3.
Line 2int size = copy * 2; creates a second object; from that point to the closing brace, size means the inner one.
Line 3Writing int size = size * 2; instead would initialize the inner size from itself, because its scope already began at its declarator, giving an indeterminate value rather than 6.
Line 4The outer size was never assigned to, only hidden, so it still prints 3 after the block.
Shadowing a parameter silently breaks a function
Demonstrates that redeclaring a parameter inside an if block updates a short-lived copy instead of the value the function returns.
<stdio.h>
static int clamp_broken(int value, int limit)
{
if (value > limit) {
int value = limit; /* a new object, not the parameter */
printf(" inner value = %d\n", value);
}
return value; /* the parameter, still 120 */
}
static int clamp_fixed(int value, int limit)
{
if (value > limit) {
value = limit; /* assignment to the parameter itself */
}
return value;
}
int main(void)
{
printf("broken: %d\n", clamp_broken(120, 100));
printf("fixed : %d\n", clamp_fixed(120, 100));
return 0;
}
Example explained
Line 1The parameter value has the scope of the whole function body, which means a nested block is allowed to declare its own value and hide it.
Line 2int value = limit; makes a second object whose lifetime ends at the if block's closing brace, so the parameter is untouched.
Line 3return value; sits outside that block, so the name resolves back to the parameter and 120 comes out.
Line 4clamp_fixed differs by one keyword: dropping int turns the declaration into an assignment to the parameter, so 100 is returned.
Lifetime is why you copy the value out
Shows a local whose storage disappears at the closing brace, and the caller-owned object that safely receives its value.
<stdio.h>
static void store_answer(int *slot)
{
int local = 42; /* storage exists only while this call runs */
*slot = local; /* copy the value out while local is alive */
}
int main(void)
{
int keep = 0; /* lives until main returns */
store_answer(&keep);
printf("keep = %d\n", keep);
return 0;
}
Example explained
Line 1local gets storage when store_answer is entered and loses it at the closing brace, so returning &local would hand back an address the program may reuse.
Line 2*slot = local; copies the value while local is still alive, which is what makes the pattern safe rather than lucky.
Line 3keep is declared in main's body, so its lifetime spans the whole call and the address passed in stays valid the entire time.
Important notes
Shadowing is legal C, not an error, so the compiler is silent about it unless you ask for -Wshadow; a shadowed variable is hidden, never modified or destroyed.
Declaring the loop variable in for (int i = 0; ...) requires C99 or later, and its scope covers the whole for statement including the body, ending when the loop ends.
Common mistakes
Declaring int total = 0; inside a loop body and expecting it to accumulate: a new object is created each iteration, so the result is only ever the contribution of one pass.
Leaving int in front of an assignment inside an if, such as int value = limit; where value is a parameter: it compiles with no default warning, and the caller receives the unmodified value.
Returning the address of a local variable or local array: its storage is reclaimed at the closing brace, so the caller dereferences a dangling pointer that often appears to work in small tests and corrupts data later.
Try it yourself
Change, predict, then run
Write a program with int limit = 10;, then a block that declares int limit = 99; and prints it, then a print of limit after the block. Run it, delete the word int from the inner declaration, and predict both output lines before running again.
Open the C workspaceCheck your understanding
What does this program print? #include <stdio.h> int main(void) { int n = 5; for (int i = 0; i < 3; i++) { int n = i; n += 10; } printf("%d\n", n); }
- 5, because the loop body's n is a separate object created and destroyed on each iteration
- 12, because the last iteration left n holding 2 + 10
- 35, because n += 10 ran three times on the outer n
- Nothing predictable, because declaring n twice in one function is undefined behaviour
Show answer
Inside the loop body, every occurrence of n resolves to the inner declaration, so the outer n is never named there and keeps its value of 5. Option 2 assumes the inner declaration and the outer variable are the same object; they are not, and the inner one's storage is gone at each closing brace. Declaring the same name in a nested block is legal C, so option 4 is wrong as well.