Variable Scope Examples: Shadowing, static Locals, extern and Dangling Pointers-Free Embedded C Tutorial

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

MODULE 4 · LESSON 4.2 · EXAMPLES

Variable Scope Examples: Shadowing, static Locals, extern and Dangling Pointers

Nine compiled examples with real output: shadowing, block scope errors, static counters, one-time init flags, stack frames, dangling pointer warnings, internal linkage and extern. Study this one of the variable scope examples before the next.

9
Worked Examples
Beginner → Intermediate
Level
Real output
Compiled and run

Variable Scope Examples: What This Page Covers

These variable scope examples are compiled and run for real. Each one isolates a single rule about where a name is visible or how long its storage lives, so you can predict the output before you read it. Another of the variable scope examples that shows the rule in output.

#ExampleRule demonstrated
1Local hides global (shadowing)Innermost declaration wins
2Nested blocks and loop variablesBlock scope ends at }
3Static local counter versus automatic localLifetime is not scope
4Local static used as a one-time init flagEmbedded idiom
5Stack frames: the same local at the same addressAutomatic storage
6Returning the address of a local (dangling pointer)Lifetime ends at return
7static global: internal linkage across two filesFile-private state
8extern across two files, shared counterExternal linkage
9Scope ladder: one name, four meaningsShadowing chain
10Mini exercisesSelf-test

Example 1: A Local Hides a Global (Shadowing)

Problem: The same name level is declared three times. Which one does each printf use? Among the variable scope examples this is the most common in interviews.

shadow.c
#include <stdio.h> int level = 10; /* global */ int main(void) { printf(“1) global level = %d\n”, level); int level = 20; /* local hides the global */ printf(“2) local level = %d\n”, level); { int level = 30; /* inner block hides the outer local */ printf(“3) block level = %d\n”, level); } printf(“4) back to local = %d\n”, level); return 0; }
Output (verified by compiling and running)
1) global level = 10 2) local level = 20 3) block level = 30 4) back to local = 20
Which declaration is visible at each line
line 1
10
global
line 2
20
main local
line 3
30
inner block
line 4
20
inner block ended
Why shadowing is dangerous: Compile with -Wshadow. A shadowed variable compiles silently, so an assignment can land in the wrong object.

Example 2: Nested Blocks and Loop Variables

Problem: A variable declared inside a loop body lives only inside it. Show both the working program and the error you get when you break the rule. These variable scope examples build on each other.

block_scope.c
#include <stdio.h> int main(void) { int total = 0; for (int i = 1; i <= 3; i++) { int square = i * i; /* created fresh on every iteration */ total += square; printf(“i=%d square=%d total=%d\n”, i, square, total); } /* printf(“%d”, i); <- error: i is out of scope here */ printf(“final total = %d\n”, total); return 0; }
Output (verified by compiling and running)
i=1 square=1 total=1 i=2 square=4 total=5 i=3 square=9 total=14 final total = 14

What the compiler says when you use a name outside its block

bad.c
#include <stdio.h> int main(void) { for (int i = 0; i < 3; i++) { int square = i * i; } printf(“%d\n”, square); return 0; }
Real GCC output
bad.c: In function ‘main’: bad.c:6:13: warning: unused variable ‘square’ [-Wunused-variable] 6 | int square = i * i; | ^~~~~~ bad.c:8:20: error: ‘square’ undeclared (first use in this function) 8 | printf(“%d\n”, square); | ^~~~~~ bad.c:8:20: note: each undeclared identifier is reported only once for each function it appears in

square was created inside the braces and destroyed at the closing brace. The compiler reports undeclared because, from the point of view of the line after the loop, the name does not exist. Fix: declare the variable in the outer block if you need it afterwards.

Example 3: A Static Local Counter Versus an Automatic Local

Problem: Two counters are incremented the same way. Why does only one of them remember its value?

static_counter.c
#include <stdio.h> static void count_calls(void) { static int s_calls = 0; /* program lifetime, block scope */ int a_calls = 0; /* new on every call */ s_calls++; a_calls++; printf(“static = %d, automatic = %d\n”, s_calls, a_calls); } int main(void) { for (int i = 0; i < 4; i++) count_calls(); /* s_calls is not visible here: block scope, but it still exists in memory */ return 0; }
Output (verified by compiling and running)
static = 1, automatic = 1 static = 2, automatic = 1 static = 3, automatic = 1 static = 4, automatic = 1
Scope and lifetime of the two counters
static int s_calls
Scope: only inside count_calls()
Lifetime: entire program
Memory: .bss (SRAM), fixed address
int a_calls
Scope: only inside count_calls()
Lifetime: one call
Memory: stack frame, released at return
Remember: Both have the same scope. They differ only in lifetime. That is the whole point of the keyword static on a local.

Example 4: A Static Local as a One-Time Initialization Flag

Problem: A driver function may be called from several places, but the hardware must be configured only once.

init_once.c
#include <stdio.h> #include <stdbool.h> static void init_hardware(void) { static bool s_done = false; /* remembers that init already ran */ if (s_done) { printf(“init skipped (already done)\n”); return; } printf(“init: enabling clock, configuring pins\n”); s_done = true; } int main(void) { init_hardware(); init_hardware(); init_hardware(); return 0; }
Output (verified by compiling and running)
init: enabling clock, configuring pins init skipped (already done) init skipped (already done)

The flag has block scope, so no other function can accidentally clear it, yet it survives between calls. On the STM32F407 this variable sits in .bss and is zero (false) after reset, which is why the first call performs the setup. The pattern is not thread safe: with an RTOS, protect it with a mutex or initialize from one task.

Example 5: Stack Frames, One Slot Reused and One Frame per Recursion

Problem: Show that automatic variables of different calls can share the same address, while nested calls get new frames below the caller.

stack_frames.c
#include <stdio.h> #include <stdint.h> static uintptr_t probe(int depth) { int marker = depth; /* a local inside every call */ (void)marker; return (uintptr_t)&marker; } static uintptr_t recurse(int depth, uintptr_t first) { int marker = depth; uintptr_t here = (uintptr_t)&marker; printf(“depth %d: this frame is %lu bytes %s the first frame\n”, depth, (unsigned long)(here > first ? here – first : first – here), here < first ? “below” : “above”); if (depth < 3) return recurse(depth + 1, first); return here; } int main(void) { uintptr_t a = probe(1), b = probe(2); printf(“two separate calls reuse the same stack slot: %s\n”, a == b ? “yes” : “no”); uintptr_t m = (uintptr_t)&a; recurse(0, m); return 0; }
Output (verified by compiling and running)
two separate calls reuse the same stack slot: yes depth 0: this frame is 36 bytes below the first frame depth 1: this frame is 100 bytes below the first frame depth 2: this frame is 164 bytes below the first frame depth 3: this frame is 228 bytes below the first frame

Absolute addresses are not printed because they differ between runs and platforms. The reproducible facts are that sequential calls reuse the freed space and that every nested call needs additional stack. On the STM32F407 the stack grows downward from the top of SRAM, so deep recursion eats the memory that sits below it and is dangerous with only 128 KB.

Stack during recursion (grows downward)
main(): a, b, m
recurse(0): marker
recurse(1): marker
recurse(2): marker
recurse(3): marker ↓ grows

Example 6: Returning the Address of a Local (Dangling Pointer)

Problem: A function returns a pointer to its local variable. Does the compiler catch it, and how do you fix it?

dangling.c
#include <stdio.h> static int *bad(void) { int local = 42; return &local; /* address of a variable that dies at return */ } static int *good(void) { static int kept = 42; /* program lifetime */ return &kept; } int main(void) { int *p = good(); printf(“good(): %d\n”, *p); int *q = bad(); (void)q; /* never dereference q */ return 0; }
dangling.c
#include <stdio.h> static int *bad(void) { int local = 42; return &local; /* address of a variable that dies at return */ } static int *good(void) { static int kept = 42; /* program lifetime */ return &kept; } int main(void) { int *p = good(); printf(“good(): %d\n”, *p); int *q = bad(); (void)q; /* never dereference q */ return 0; }
GCC output with -Wall -Wextra
dangling.c: In function ‘bad’: dangling.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr] 6 | return &local; /* address of a variable that dies at return */ | ^~~~~~

GCC warns function returns address of local variable. Dereferencing such a pointer is undefined behaviour: the memory may already hold another function’s locals. In good() the variable is static, so its lifetime covers the whole program and the pointer stays valid. Other safe options are to let the caller pass in a buffer or to allocate statically.

good_only.c
#include <stdio.h> static int *bad(void) { int local = 42; return &local; /* address of a variable that dies at return */ } static int *good(void) { static int kept = 42; /* program lifetime */ return &kept; } int main(void) { int *p = good(); printf(“good(): %d\n”, *p); return 0; }
Output (verified by compiling and running)
good(): 42

Example 7: static at File Scope Gives Internal Linkage

Problem: Keep a counter private to one module. Only the module’s own functions may change it.

counter.c
#include “counter.h” static int s_count = 0; /* internal linkage: private to this file */ void counter_inc(void) { s_count++; } int counter_get(void) { return s_count; }
counter.h
#ifndef COUNTER_H #define COUNTER_H void counter_inc(void); int counter_get(void); #endif
main.c
#include <stdio.h> #include “counter.h” int main(void) { counter_inc(); counter_inc(); counter_inc(); printf(“count = %d\n”, counter_get()); return 0; }
Output (verified by compiling and running)
count = 3

What happens when another file tries to reach it

counter.c
#include “counter.h” static int s_count = 0; /* internal linkage: private to this file */ void counter_inc(void) { s_count++; } int counter_get(void) { return s_count; }
counter.h
#ifndef COUNTER_H #define COUNTER_H void counter_inc(void); int counter_get(void); #endif
main.c
#include <stdio.h> #include “counter.h” extern int s_count; /* try to reach the private variable */ int main(void) { counter_inc(); printf(“count = %d\n”, s_count); return 0; }
Real GCC/ld output
/usr/bin/ld: file1.o: in function `main’: main.c:(.text+0xf): undefined reference to `s_count’ collect2: error: ld returned 1 exit status

Because s_count is static, the name is not exported to the linker, so main.c cannot link to it. This is encapsulation in plain C: the data is hidden behind counter_inc() and counter_get(), and you can change the representation later without touching callers.

Example 8: extern Across Two Files, One Shared Variable

Problem: An interrupt routine in one file counts events and the main loop in another file reads them. Both must use the same object.

shared.h
#ifndef SHARED_H #define SHARED_H extern int g_events; /* declaration */ void isr_simulate(void); #endif
shared.c
#include “shared.h” int g_events = 0; /* the single definition, external linkage */ void isr_simulate(void) { g_events++; }
main.c
#include <stdio.h> #include “shared.h” int main(void) { isr_simulate(); isr_simulate(); g_events += 10; /* main.c writes the same object */ printf(“g_events = %d\n”, g_events); return 0; }
Output (verified by compiling and running)
g_events = 12
On real firmware: A variable written in an ISR and read in main() must also be declared volatile, otherwise the optimiser may keep it in a register. This host demo calls the function normally, so volatile is not required here. Module 14 covers it in depth.
LinkageHow to write itVisible to the linker?
ExternalFile-scope int g_events; (plus extern elsewhere)Yes
InternalFile-scope static int s_count;No
NoneAny local variableNo

Example 9: The Scope Ladder, One Name With Four Meanings

Problem: The identifier x refers to four different objects in one file. Trace which is which, including the static inside the inner block that keeps counting across calls.

ladder.c
#include <stdio.h> int x = 1; /* file scope */ static void show(void) { printf(“show(): x = %d\n”, x); /* sees the global */ } static void hide(void) { int x = 2; /* function-level local */ printf(“hide(): x = %d\n”, x); { static int x = 3; /* static, block scope */ x++; printf(“hide() blk: x = %d\n”, x); } printf(“hide() end: x = %d\n”, x); } int main(void) { show(); hide(); hide(); x = 100; /* the global again */ show(); return 0; }
Output (verified by compiling and running)
show(): x = 1 hide(): x = 2 hide() blk: x = 4 hide() end: x = 2 hide(): x = 2 hide() blk: x = 5 hide() end: x = 2 show(): x = 100
WhereObjectLifetime
show() and main()global xwhole program
hide() outerlocal x = 2one call
hide() inner blockstatic x (3, 4, …)whole program
hide() after the blocklocal x = 2 againone call

Real projects should never rely on this. It is shown to make the lookup rule concrete: the compiler searches outward from the innermost block until it finds a declaration.

Example 10: Mini Exercises

Exercise 1: A function has static int n = 0; n++;. After 5 calls, what is n?
5. The initializer runs once and the value is kept.
Exercise 2: Same code without static. After 5 calls?
Each call starts at 0 and ends at 1, so n is always 1 when the function returns.
Exercise 3: Can another file use a static int count; defined at file scope?
No. It has internal linkage; an extern declaration elsewhere causes an undefined reference.
Exercise 4: What warning flag finds a local that hides a global?
-Wshadow.
Exercise 5: Is returning &local from a function ever correct?
No, unless the variable is static (or otherwise outlives the call).
Exercise 6: Where is a static local stored on the STM32F407?
In .data if it has a non-zero initializer, otherwise in .bss, both in SRAM. Never on the stack.
Exercise 7: Which has bigger scope, a global or a static global?
They have the same scope (the file), but only the plain global has external linkage.

Key Takeaways from the Variable Scope Examples

  • The variable scope examples show that the innermost declaration always wins, and -Wshadow reveals accidents.
  • Block scope ends at the closing brace; using the name afterwards is a compile error.
  • A static local has block scope but program lifetime: it is the safe way to keep state private to one function.
  • Never return the address of an automatic variable: GCC warns, and the behaviour is undefined.
  • A file-scope static is private to its file; an ordinary global is shared through extern.
  • Static locals and globals live in SRAM sections (.data/.bss); automatic locals live on the stack.

Ready for the quiz?

Next: predict-the-output scope questions, code challenges and interview answers on scope and lifetime.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *