Module 3.3

Variable Scope

Understand where variables live and how long they exist in C programs. Master local, global, static, and external variables for clean, bug-free code!

30 min read
Beginner
Hands-on Examples
What You'll Learn
  • Local variable scope and lifetime
  • Global variables and their risks
  • Static variables for state persistence
  • External linkage with extern
  • Best practices for variable scope
Contents
01

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.

Definition: A local variable is a variable declared inside a function or block that can only be used within that specific scope. It has automatic storage duration, meaning the computer automatically creates memory for it when entering the block and frees that memory when leaving. Local variables start with garbage values (random data) unless you explicitly initialize them. They are stored on the stack, a special area of memory that grows and shrinks as functions are called and return.

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
Key Insight: Local variables are stored on the stack. Each function call gets its own copy, making recursion possible.

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
Warning: Avoid variable shadowing as it makes code confusing. Use distinct names for variables in nested scopes.
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
02

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.

Definition: A global variable is declared outside all functions and has file scope, meaning it can be accessed from anywhere in that source file. Unlike local variables, global variables exist for the entire lifetime of the program - from the moment your program starts until it ends. They are stored in a special memory area called the data segment (not the stack). An important beginner tip: global variables are automatically initialized to zero (or NULL for pointers) if you don't give them a value, unlike local variables which contain garbage.

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
Key Insight: Global variables are stored in the data segment, not the stack. They are initialized to zero if no explicit value is given.

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
Warning: Global variables create "action at a distance" - changes in one part of code affect another part unexpectedly. This makes debugging nightmares.

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
03

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.

Definition: A static local variable is declared using the 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)
Key Insight: The line 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!
Warning: Static variables in functions make functions non-reentrant and not thread-safe. Avoid them in multi-threaded code.
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
04

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.

Definition: The 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!
Key Insight: A variable must be defined in exactly one file, but can be declared with 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;
}
Warning: Never put definitions (with initialization) in header files - this causes "multiple definition" linker errors when included in multiple files.
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
05

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.

Definition: A storage class in C determines four things about a variable: (1) where it is stored in memory, (2) its initial value if not initialized, (3) its scope (where it can be accessed), and (4) its lifetime (how long it exists). The four storage classes are: 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
Key Insight: The 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
Important Limitation: You cannot take the address of a 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 extern can be accessed across multiple files
Best Practice: Always use the smallest scope possible. Prefer local variables over global, and avoid 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

Interactive

Click "Call Function" to see how local, static, and global variables behave across multiple function calls.

Local Variable
0
Resets each call
Static Variable
0
Persists between calls
Global Variable
0
Shared everywhere

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.

1 int globalX = 100;
2
3 void myFunc() {
4 int localA = 10;
5 static int staticB = 0;
6 {
7 int blockC = 5;
8 }
9 }
Visible Variables
Click a line to explore

Select any line in the code to see which variables are in scope at that point.

05

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

1 Where are local variables stored in memory?
2 What happens to a static local variable when its function exits?
3 What is the main risk of using global variables?
4 What does the extern keyword do?
5 Which scope type should you prefer for most variables?
6 What is the initial value of a global variable if not explicitly initialized?
0/6 answered