Local Variables
Local variables are declared inside a function or block (code between curly braces { }) and can only be accessed within that specific area. Think of them like notes you write on a whiteboard during a meeting - once the meeting ends, the whiteboard is erased. They are created when the program enters the block and automatically destroyed when the block exits. This is the most common and safest type of variable in C programming because other parts of your code cannot accidentally change them.
Syntax:
void function_name() {
data_type variable_name; // Uninitialized (garbage value)
data_type variable_name = value; // Initialized
}
Basic Local Variable Example
When you declare a variable inside a function, it only exists while that function is executing. Other functions cannot access it, which prevents accidental modifications.
#include <stdio.h>
void myFunction() {
int localVar = 10; // Local to myFunction
printf("Inside function: %d\n", localVar);
}
int main() {
myFunction();
// printf("%d", localVar); // ERROR: localVar not accessible here
return 0;
}
Output:
Inside function: 10
Block Scope
Variables can also be local to any block (code between curly braces), not just functions. This is useful for limiting variable lifetime to exactly where it is needed.
#include <stdio.h>
int main() {
int outer = 5;
{
int inner = 10; // Only exists in this block
printf("Inner: %d, Outer: %d\n", inner, outer);
}
// printf("%d", inner); // ERROR: inner is out of scope
printf("Outer: %d\n", outer);
return 0;
}
Output:
Inner: 10, Outer: 5
Outer: 5
Variable Shadowing
When a local variable has the same name as a variable in an outer scope, the inner variable "shadows" the outer one. This can lead to bugs if not used carefully.
#include <stdio.h>
int main() {
int x = 100;
printf("Outer x: %d\n", x);
{
int x = 200; // Shadows outer x
printf("Inner x: %d\n", x);
}
printf("Outer x again: %d\n", x); // Still 100
return 0;
}
Output:
Outer x: 100
Inner x: 200
Outer x again: 100
| Property | Local Variable |
|---|---|
| Storage | Stack |
| Lifetime | Block entry to exit |
| Default Value | Undefined (garbage) |
| Visibility | Within declaring block only |
Practice Questions: Local Variables
Test your understanding of local variable scope.
void test() {
int a = 5;
printf("%d\n", a);
}
int main() {
test();
test();
return 0;
}
Show Solution
// Output: 5
// Output: 5
// Each call creates a fresh local variable
int* getPointer() {
int local = 42;
return &local; // Bug!
}
int main() {
int *ptr = getPointer();
printf("%d\n", *ptr);
return 0;
}
Show Solution
// Bug: Returning pointer to local variable
// local is destroyed when getPointer exits
// Dereferencing ptr is undefined behavior
int main() {
int i = 10;
for (int i = 0; i < 3; i++) {
printf("%d ", i);
}
printf("- %d\n", i);
return 0;
}
Show Solution
// Output: 0 1 2 - 10
// The for loop has its own i that shadows outer i
// Outer i remains unchanged at 10
Global Variables
Global variables are declared outside all functions, typically at the top of your source file. They can be accessed and modified from any function in the same file, making them seem convenient at first. However, this convenience comes with a serious cost: when any function can change a global variable, it becomes very difficult to track down bugs. Imagine a shared notebook that anyone in your office can write in - if something gets erased, who did it? Use global variables sparingly and only when truly necessary.
Syntax:
// Declared outside all functions (at file level)
data_type variable_name; // Auto-initialized to 0
data_type variable_name = value; // Explicitly initialized
void function1() { /* can access variable_name */ }
void function2() { /* can also access variable_name */ }
Basic Global Variable Example
Global variables are accessible from any function in the same file. They persist for the entire lifetime of the program.
#include <stdio.h>
int counter = 0; // Global variable
void increment() {
counter++;
}
void printCounter() {
printf("Counter: %d\n", counter);
}
int main() {
increment();
increment();
increment();
printCounter();
return 0;
}
Output:
Counter: 3
Why Global Variables Are Risky
Global variables seem convenient but create serious problems in larger programs. Any function can modify them, making bugs hard to track down.
#include <stdio.h>
int total = 0; // Global - anyone can change it!
void addToTotal(int amount) {
total += amount;
}
void resetTotal() {
total = 0; // Might be called unexpectedly
}
int main() {
addToTotal(50);
addToTotal(30);
resetTotal(); // Oops! Someone reset our total
printf("Total: %d\n", total); // Surprise: 0!
return 0;
}
Output:
Total: 0
When Globals Might Be Acceptable
Despite the risks, there are a few cases where global variables are reasonable:
- Constants: Read-only configuration values (use
const) - Hardware registers: In embedded systems with fixed memory addresses
- Very small programs: Simple utilities where complexity is minimal
#include <stdio.h>
const double PI = 3.14159265359; // Global constant - safe!
const int MAX_USERS = 100;
int main() {
double area = PI * 5 * 5;
printf("Circle area: %.2f\n", area);
return 0;
}
Output:
Circle area: 78.54
| Property | Global Variable |
|---|---|
| Storage | Data segment |
| Lifetime | Entire program |
| Default Value | Zero |
| Visibility | Entire file (or more with extern) |
Practice Questions: Global Variables
Test your understanding of global variable scope.
int globalNum; // No initialization
int main() {
printf("%d\n", globalNum);
return 0;
}
Show Solution
// Output: 0
// Global variables are zero-initialized by default
int x = 10; // Global
void func() {
int x = 20; // Local
printf("%d ", x);
}
int main() {
func();
printf("%d\n", x);
return 0;
}
Show Solution
// Output: 20 10
// Local x shadows global x in func()
// main() accesses the global x
int val = 1;
void double_it() { val *= 2; }
void add_one() { val += 1; }
int main() {
double_it(); // val = ?
add_one(); // val = ?
double_it(); // val = ?
printf("%d\n", val);
return 0;
}
Show Solution
// Output: 6
// 1 * 2 = 2, 2 + 1 = 3, 3 * 2 = 6
// Global val is modified by each function
Static Variables
Static variables have a special superpower: they remember their value between function calls. Unlike regular local variables that are created fresh each time a function runs, a static variable keeps its value even after the function ends. Think of it like a tally counter - every time you press it, it remembers the count from before. Static variables combine the best of both worlds: they have the long lifetime of global variables (lasting the entire program), but they stay safely hidden inside their function like local variables.
static keyword inside a function. It retains its value between function calls and is initialized only once - the very first time the function is called. After that, the initialization line is skipped on subsequent calls. Like global variables, static variables are stored in the data segment (not the stack) and are automatically initialized to zero if you don't provide a value. They are perfect for counting function calls, caching expensive calculations, or maintaining state without exposing it to other functions.
Syntax:
void function_name() {
static data_type variable_name; // Auto-initialized to 0 (only once)
static data_type variable_name = value; // Initialized once, retains value
}
Counting Function Calls
The classic use of static variables is counting how many times a function has been called.
#include <stdio.h>
void countCalls() {
static int count = 0; // Initialized only once!
count++;
printf("This function has been called %d time(s)\n", count);
}
int main() {
countCalls();
countCalls();
countCalls();
return 0;
}
Output:
This function has been called 1 time(s)
This function has been called 2 time(s)
This function has been called 3 time(s)
static int count = 0; runs only once, not on every function call. The variable remembers its value.
Static vs Regular Local Variables
Compare the behavior of regular and static local variables to see the difference clearly:
#include <stdio.h>
void regularVar() {
int regular = 0; // Created fresh each call
regular++;
printf("Regular: %d\n", regular);
}
void staticVar() {
static int persistent = 0; // Persists between calls
persistent++;
printf("Static: %d\n", persistent);
}
int main() {
for(int i = 0; i < 3; i++) {
regularVar();
staticVar();
}
return 0;
}
Output:
Regular: 1
Static: 1
Regular: 1
Static: 2
Regular: 1
Static: 3
One-Time Initialization
Static variables are perfect for expensive operations that should run only once:
#include <stdio.h>
int expensiveCalculation() {
printf("Performing expensive calculation...\n");
return 42; // Simulated result
}
int getValue() {
static int cached = 0;
static int initialized = 0;
if (!initialized) {
cached = expensiveCalculation();
initialized = 1;
}
return cached;
}
int main() {
printf("First call: %d\n", getValue());
printf("Second call: %d\n", getValue());
printf("Third call: %d\n", getValue());
return 0;
}
Output:
Performing expensive calculation...
First call: 42
Second call: 42
Third call: 42
Static in Global Scope
When used at file scope, static limits a variable's visibility to that file only,
preventing it from being accessed from other files.
// In file1.c
static int privateCounter = 0; // Only visible in this file
void incrementPrivate() {
privateCounter++;
}
// In file2.c - cannot access privateCounter!
| Property | Regular Local | Static Local |
|---|---|---|
| Lifetime | Function call | Entire program |
| Initialization | Every call | Only once |
| Storage | Stack | Data segment |
| Default Value | Garbage | Zero |
Practice Questions: Static Variables
Test your understanding of static variable behavior.
void test() {
static int n = 5;
n++;
printf("%d ", n);
}
int main() {
test(); test(); test();
return 0;
}
Show Solution
// Output: 6 7 8
// n starts at 5, increments each call
// Static means n persists between calls
void funcA() {
static int x = 0;
x++;
printf("A:%d ", x);
}
void funcB() {
static int x = 0;
x += 2;
printf("B:%d ", x);
}
int main() {
funcA(); funcB(); funcA(); funcB();
return 0;
}
Show Solution
// Output: A:1 B:2 A:2 B:4
// Each function has its OWN static x
// They don't interfere with each other
int recurse(int n) {
static int sum = 0;
if (n > 0) {
sum += n;
recurse(n - 1);
}
return sum;
}
int main() {
printf("%d ", recurse(3));
printf("%d\n", recurse(2));
return 0;
}
Show Solution
// Output: 6 9
// First call: 3+2+1 = 6
// Second call: sum is STILL 6, adds 2+1 = 9
// Bug! Static persists across ALL calls
External Variables
As your C programs grow larger, you will split them into multiple source files (like main.c, utils.c, config.c). But what if you need to share a variable between these files? That is where the extern keyword comes in. It tells the compiler: "Hey, this variable exists somewhere else - don't create a new one, just let me use the existing one." Think of it like a phone extension at work - you are not creating a new phone line, just connecting to an existing one in another office.
extern keyword is used to declare a variable that is defined in another source file. This is a crucial distinction in C: a definition creates the variable and allocates memory for it (like int count = 0;), while a declaration just announces that the variable exists somewhere else (like extern int count;). You can have only ONE definition across all your files, but you can have many extern declarations. The extern keyword does not allocate new memory - it simply creates a reference to the original variable so you can access it from a different file.
Syntax:
// file1.c - Definition (creates the variable)
data_type variable_name = value;
// file2.c - Declaration (references the variable)
extern data_type variable_name; // No memory allocated, just a reference
Understanding Declaration vs Definition
The key to understanding extern is the difference between declaration and definition:
- Definition: Creates the variable and allocates memory (
int count = 0;) - Declaration: Says "this variable exists somewhere" (
extern int count;)
// main.c - Definition (creates the variable)
int sharedValue = 100;
// other.c - Declaration (references the variable)
extern int sharedValue; // No memory allocated here!
extern in many files.
Multi-File Example
Here's a complete example with two source files sharing a global counter:
// counter.c - Contains the definition
#include <stdio.h>
int globalCounter = 0; // Definition with memory
void incrementCounter() {
globalCounter++;
}
void printCounter() {
printf("Counter: %d\n", globalCounter);
}
// main.c - Uses extern declaration
#include <stdio.h>
extern int globalCounter; // Declaration only
void incrementCounter(); // Function prototype
void printCounter();
int main() {
printf("Initial: %d\n", globalCounter);
incrementCounter();
incrementCounter();
printCounter();
globalCounter = 50; // Direct access works!
printCounter();
return 0;
}
Compile and run:
gcc main.c counter.c -o program
./program
Output:
Initial: 0
Counter: 2
Counter: 50
Using Header Files
The best practice is to put extern declarations in header files:
// config.h - Header file with declarations
#ifndef CONFIG_H
#define CONFIG_H
extern int debugMode;
extern char appName[];
extern const int MAX_CONNECTIONS;
void initConfig(void);
#endif
// config.c - Source file with definitions
#include "config.h"
int debugMode = 0;
char appName[] = "MyApp";
const int MAX_CONNECTIONS = 100;
void initConfig(void) {
debugMode = 1;
}
// main.c - Uses the shared variables
#include <stdio.h>
#include "config.h"
int main() {
initConfig();
printf("App: %s, Debug: %d\n", appName, debugMode);
return 0;
}
| Concept | Syntax | Memory? | Where to Use |
|---|---|---|---|
| Definition | int x = 5; |
Yes | One .c file |
| Declaration | extern int x; |
No | Header files / other .c files |
| Static global | static int x; |
Yes | Private to one file |
Practice Questions: External Variables
Test your understanding of extern and multi-file scope.
Which line is a definition and which is a declaration?
int value = 10;
extern int value;
Show Solution
// int value = 10; → Definition (allocates memory)
// extern int value; → Declaration (references only)
// Definition must appear in exactly ONE file
// shared.h
int count = 0; // Hmm...
// file1.c
#include "shared.h"
// file2.c
#include "shared.h"
Show Solution
// Error: multiple definition of 'count'
// The header is included in both files
// Each file creates its own 'count'
// Fix: Use "extern int count;" in header
// Put "int count = 0;" in ONE .c file
// file1.c
static int x = 5;
int y = 10;
// file2.c
extern int x;
extern int y;
int main() {
printf("%d %d\n", x, y);
return 0;
}
Show Solution
// Linker error: undefined reference to 'x'
// static int x is PRIVATE to file1.c
// extern int x in file2.c cannot find it
// y works fine (not static)
// Fix: Remove static from x, or don't extern it
Storage Classes Overview
C has four storage class specifiers that control how variables are stored, their lifetime, and their visibility. You have already learned about static and extern in detail. Now let's complete the picture with auto and register, and see how they all compare.
auto, register, static, and extern.
Syntax:
auto int x = 10; // Automatic (default for local)
register int i; // Request CPU register storage
static int count = 0; // Persistent local variable
extern int shared; // Reference to external variable
The auto Storage Class
The auto keyword is the default storage class for local variables. You almost never need to write it explicitly because all local variables are auto by default. It means "automatic storage duration" - the variable is automatically created when entering the block and destroyed when leaving.
#include <stdio.h>
int main() {
auto int x = 10; // Same as: int x = 10;
int y = 20; // Also auto by default
printf("x = %d, y = %d\n", x, y);
return 0;
}
Output:
x = 10, y = 20
auto keyword is rarely used in modern C because it is the default. However, in C++11 and later, auto has a completely different meaning (automatic type deduction), so be aware of this if you switch between C and C++.
The register Storage Class
The register keyword is a hint to the compiler that a variable will be used frequently, so it should be stored in a CPU register instead of RAM for faster access. However, modern compilers are very smart and usually ignore this hint because they can optimize better than humans.
#include <stdio.h>
int main() {
register int counter; // Hint: store in CPU register
// Fast loop with register variable
for (counter = 0; counter < 1000000; counter++) {
// Do something
}
printf("Counted to %d\n", counter);
return 0;
}
Output:
Counted to 1000000
register variable using & because CPU registers don't have memory addresses. This code would cause a compile error: int *ptr = &counter;
Complete Storage Classes Comparison
| Storage Class | Keyword | Storage Location | Default Value | Scope | Lifetime |
|---|---|---|---|---|---|
| Automatic | auto |
Stack | Garbage (random) | Block/Function | Until block ends |
| Register | register |
CPU Register (or Stack) | Garbage (random) | Block/Function | Until block ends |
| Static | static |
Data Segment | Zero | Block (local) or File (global) | Entire program |
| External | extern |
Data Segment | Zero | Multiple files | Entire program |
Quick Reference: Scope Rules
Understanding scope rules helps you predict which variables are visible at any point in your code:
- Block Scope: Variables declared inside
{ }are only visible within that block - Function Scope: Labels (for
goto) are visible throughout the function - File Scope: Variables declared outside all functions are visible in the entire file
- Program Scope: Variables with
externcan be accessed across multiple files
register in modern code - let the compiler optimize for you.
Practice Questions: Storage Classes
Test your understanding of all four storage classes.
void func() {
int x = 5; // What storage class?
}
Show Solution
// Storage class: auto (automatic)
// All local variables are auto by default
// Same as writing: auto int x = 5;
register int count = 10;
int *ptr = &count; // What happens?
Show Solution
// Compile Error!
// Cannot take address of register variable
// CPU registers don't have memory addresses
// Fix: Remove 'register' keyword
int a; // Storage class?
static int b; // Storage class?
extern int c; // Storage class?
void func() {
int d; // Storage class?
static int e; // Storage class?
}
Show Solution
int a; // extern (global, file scope)
static int b; // static (global, file-private)
extern int c; // extern (declaration only)
void func() {
int d; // auto (local, stack)
static int e; // static (local, persists)
}
Interactive Demos
Explore variable scope concepts interactively. These demos visualize how different variable types behave during program execution.
Variable Lifetime Simulator
InteractiveClick "Call Function" to see how local, static, and global variables behave across multiple function calls.
Local Variable
Static Variable
Global Variable
Click "Call Function" to simulate calling a function that increments each variable type.
Scope Visibility Explorer
Explore!Click on different code regions to see which variables are visible (in scope) at each location.
Visible Variables
Select any line in the code to see which variables are in scope at that point.
Key Takeaways
Local Scope
Variables declared inside functions are only accessible within that function and are destroyed when it exits.
Global Scope
Global variables are accessible everywhere but create hidden dependencies. Use sparingly.
Static Persistence
Static variables retain their value between calls, perfect for counters and state.
External Linkage
Use extern to share variables across files while keeping a single definition.
Lifetime Matters
Understand when variables are created and destroyed to avoid bugs and memory issues.
Minimize Scope
Always use the smallest scope possible for cleaner, safer, more maintainable code.
Knowledge Check
Quick Quiz
Test your understanding of C variable scope