Storage Classes & Scope

Where a variable lives, how long it survives, and who can see it are governed by scope and storage class — four keywords (auto, static, extern, register) that control lifetime and visibility independently of a variable's type.

Every identifier in a C program raises three separate questions that are easy to blur together: where in the source code can this name be used (its scope), how long does the object behind that name actually exist while the program runs (its storage duration), and can another declaration elsewhere in the program refer to that same entity (its linkage). The storage-class specifiers — auto, register, static, extern, and _Thread_local — are the keywords that let you influence those answers directly instead of always accepting whatever the default happens to be for a given declaration. This page works through all three ideas in one pass: the four kinds of scope, the four storage durations, how linkage connects declarations across files, and the specific keywords that tie all of it together, ending with the classic static-local counter, the extern-across-files project pattern, and a full reference table.

Scope: Where a Name Can Be Used

Scope is simply the region of source code in which a particular identifier can be referred to by name. It answers a purely textual question — where is this name legal to write? — and says nothing yet about memory, timing, or how long anything actually exists.

global can be referred to from a large portion of the file, including from inside main. local, on the other hand, can only be referred to inside the block where it is declared — main's body.

#include <stdio.h>

int global = 10;

int main(void) {
    int local = 20;
    printf("%d\n", global);
    printf("%d\n", local);
    return 0;
}

The Four Kinds of Scope

C recognizes four kinds of identifier scope: block scope, function scope, function-prototype scope, and file scope. Each one describes visibility of an identifier — not how long the object behind it lives, which is a separate property covered later on this page.

Block scope covers ordinary local variables, function scope is reserved specifically for labels, function-prototype scope covers parameter names written inside a bare prototype, and file scope covers anything declared outside every function. The next several sections take these one at a time.

Block Scope

An identifier declared inside a block — a region enclosed by a matching pair of braces — has block scope: it is visible from its declaration to the end of that same block, including any blocks nested inside it.

x is declared in main's own block, so it is visible throughout main, including inside the nested inner block. y is declared inside that inner block, so its visibility never extends past the inner block's closing brace.

int main(void) {
    int x = 10;
    {
        int y = 20;
        printf("%d\n", x);
        printf("%d\n", y);
    }
    printf("%d\n", x);
    return 0;
}

Nested Blocks and Visibility

Because an inner block sits entirely inside its enclosing block, code inside the inner block can see every name that is visible in the outer block at that point — the reverse is never true.

The inner block can see x because x belongs to an outer, still-active scope. Nothing about the reverse relationship holds, though.

By the time the outer printf runs, y's block has already ended and its name has gone out of scope. This will not compile — y simply is not a visible identifier at that point in the source.

int main(void) {
    int x = 10;
    {
        int y = 20;
        printf("%d\n", x);  // valid
        printf("%d\n", y);  // valid
    }
    return 0;
}
int main(void) {
    {
        int y = 20;
    }
    printf("%d\n", y);      // y is out of scope here
    return 0;
}

Variable Shadowing

Because inner scopes can declare their own names, an inner declaration is allowed to reuse a name that already belongs to an outer, active scope. Where both are visible, the inner declaration hides — shadows — the outer one; the name simply resolves to whichever declaration is nearest.

There are two separate objects here, both named x: the file-scope global and main's own local. Inside main, the local x is the nearer declaration, so it wins and the program prints 20.

The outer x has not disappeared or been overwritten — it still exists at file scope with the value 10 — but inside main, the name x refers exclusively to the local declaration for as long as that local stays in scope.

int x = 10;

int main(void) {
    int x = 20;
    printf("%d\n", x);
    return 0;
}

Function Scope: Labels and goto

Function scope is a narrower idea than it sounds — in standard C it applies specifically to labels used as goto targets, not to ordinary variables.

The label positive: has function scope: it can be referenced by a goto anywhere within test, including from before its own textual position in the source, which is exactly what happens here.

Function scope belongs only to labels. A variable declared anywhere inside a function still follows ordinary block-scope rules, so it's worth keeping the two ideas separate even though both are sometimes loosely described as 'scoped to the function'.

#include <stdio.h>

void test(int x) {
    if (x > 0)
        goto positive;
    printf("Negative or zero\n");
    return;
positive:
    printf("Positive\n");
}

int main(void) {
    test(5);
    test(-3);
    return 0;
}

Function Prototype Scope

Parameter names written inside a bare function prototype — a declaration with no accompanying function body — have function-prototype scope: they exist only for documentation purposes within that one declaration and are not usable anywhere else.

The identifiers a and b are meaningful only inside the parentheses of that one prototype. They tell a reader what each parameter represents but create no usable name anywhere in main or elsewhere.

Because prototype parameter names carry no scope beyond the prototype itself, they are frequently omitted entirely: int add(int, int); is exactly as valid as writing int add(int a, int b);, and compiles to the same declaration.

int add(int a, int b);

int main(void) {
    printf("%d\n", a);   // a is not visible here
    return 0;
}

File Scope

An identifier declared outside every function — at the top level of a source file — has file scope. Its visibility extends from its point of declaration all the way to the end of that translation unit, reaching every function defined below it.

global_count is declared outside every function, so both increment and main can refer to it directly by name with no parameter or return value needed to hand it between them.

#include <stdio.h>

int global_count = 0;

void increment(void) {
    global_count++;
}

int main(void) {
    increment();
    printf("%d\n", global_count);
    return 0;
}

Scope Does Not Mean Lifetime

This is one of the most important distinctions in this whole topic, and it is easy to blur if you have not seen it stated explicitly: scope and storage duration answer two completely different questions.

x has block scope — its name can only be used inside test's body — and, being an ordinary local with no storage-class keyword, it normally has automatic storage duration as well. Those are two separate facts about the same declaration.

Scope answers where can the name x be used, which here is only inside test. Storage duration answers how long does the object that x names actually exist, which is a question about the running program, not about the source text — and, as the next several sections show, a name's scope can be far narrower than its object's actual lifetime.

A very common mistake is treating 'x is only visible inside this function' and 'x only exists while this function is running' as the same statement. They usually happen to agree for an ordinary local variable, but as soon as static enters the picture, they diverge sharply — a static local variable keeps a block-restricted scope while its underlying object survives for the entire program.

void test(void) {
    int x = 10;
}

The Four Storage Durations

Where scope is about visibility, storage duration is about lifetime: how long an object continues to exist while the program executes. C recognizes four storage-duration categories — automatic, static, allocated, and thread — and the first three are the ones ordinary C programs use constantly.

Allocated storage duration belongs to objects obtained at run time through functions such as malloc; it is a property of how the object was obtained, not something chosen with a storage-class keyword the way automatic, static, and thread duration are.

Automatic Storage Duration

An object with automatic storage duration is, in the ordinary case, created when execution enters the block where it is declared and ceases to exist the moment execution leaves that block.

x normally has automatic storage duration. Each call to test enters the block afresh, so each call gets its own newly created object named x, initialized to 10 all over again — nothing from one call's x carries into the next call's x.

#include <stdio.h>

void test(void) {
    int x = 10;
    printf("%d\n", x);
}

int main(void) {
    test();
    test();
    return 0;
}

The auto Storage-Class Specifier

The auto keyword can be written explicitly to specify automatic storage duration for a block-scope object, but it is almost never seen in real code because an ordinary block-scope declaration already has automatic storage duration by default.

Writing auto here changes nothing observable: void test(void) { int x = 10; } already means exactly the same thing, since automatic storage duration is the default for a plain block-scope declaration.

auto int x = 10; and int x = 10; describe the same automatic-storage-duration object. It is a mistake to read auto as a request to 'optimize this variable' the way register is sometimes (also incorrectly) read — auto only ever names the ordinary, default storage duration that block-scope variables already have.

void test(void) {
    auto int x = 10;
}

Static Storage Duration

An object with static storage duration exists for the entire execution of the program — it is created before the program's other code starts running and remains until the program terminates. File-scope objects fall into this category automatically, and static in an appropriate context also produces it.

global has static storage duration simply by virtue of being declared at file scope. Its lifetime extends from program startup to program termination, regardless of when or whether any function ever touches it.

Static storage duration is a statement about lifetime, not a guarantee about physical memory. The language only promises the object exists for the whole program run — where the implementation actually places it is a detail left to the compiler.

int global;

static Local Variables: Persisting Across Calls

One of the most important uses of static appears inside a function body, on an otherwise ordinary local variable. Adding static changes nothing about scope — the name is still only usable inside that function — but it gives the object static storage duration instead of automatic, so it is created and initialized once, and every later call finds the very same object left over from before.

count is initialized to 0 only the first time counter runs. Every subsequent call increments the same surviving object rather than starting over, so the calls print 1, then 2, then 3.

First call: count is created and initialized to 0, then incremented to 1 and printed.

Second call: count's initializer does not run again — the surviving object still holds 1, which becomes 2 and is printed.

Third call: the surviving object holds 2, which becomes 3 and is printed.

Remove static and count goes back to automatic storage duration: a brand-new object is created and reset to 0 on every call, so all three calls independently print 1 — the persistence is entirely gone.

This is exactly what a static local variable is asked to do between calls: because its object survives for the whole program rather than being torn down when the function returns, it retains whatever value the previous call left in it.

#include <stdio.h>

void counter(void) {
    static int count = 0;
    count++;
    printf("%d\n", count);
}

int main(void) {
    counter();
    counter();
    counter();
    return 0;
}
#include <stdio.h>

void counter(void) {
    int count = 0;
    count++;
    printf("%d\n", count);
}

int main(void) {
    counter();
    counter();
    counter();
    return 0;
}

Static Local Variables: Scope vs Lifetime

The counter function is the clearest possible illustration of scope and lifetime pulling apart, which is exactly why it is such a standard teaching example.

count has block scope — the name count can only be used inside counter's body, and no code outside this function can refer to it directly — yet its storage duration is the entire program execution.

So although the name count is confined to a single function, the object it names keeps existing and keeps its value across every call to that function, from the first call all the way to the last. Scope restricts where you can write the name; storage duration decides how long the thing behind the name is actually there — and a static local variable is the case where those two answers are most obviously different from each other.

void counter(void) {
    static int count = 0;
    count++;
}

Static Local Initialization Happens Only Once

A static local variable's initializer is evaluated exactly once — the first time control reaches that declaration — and is never re-run on any later call, no matter how many times the function executes afterward.

The literal 10 is only ever assigned into x the first time test runs. Every later call skips the initializer entirely and works with whatever value x was left holding.

First call: x does not yet exist, so it is created and initialized to 10, then incremented to 11.

Second call: x already exists holding 11 — the initializer is skipped — and it becomes 12.

Third call: x already exists holding 12, and becomes 13.

void test(void) {
    static int x = 10;
    x++;
}

static at File Scope: Internal Linkage

static carries a second, quite different meaning when it appears on a declaration at file scope: instead of changing storage duration (file-scope objects already have static storage duration by default), it gives the identifier internal linkage — the name becomes usable only within the translation unit that declares it.

counter here already had static storage duration simply from being at file scope. What static adds is internal linkage: no other source file can refer to this particular counter object by that name, even with a matching extern declaration.

It's a mistake to assume static always means 'local to a function' — its effect is entirely dependent on where it appears. static int x; at file scope restricts linkage to one translation unit; static int y; inside a function body instead grants the object static storage duration and gives it no linkage at all, since a purely local name was never linked to anything in the first place. The keyword is the same; the property it changes is not.

static int counter = 0;

File-Scope static: Scope, Duration, and Linkage Together

Putting the previous few sections side by side makes the contrast concrete. A file-scope static object has file scope, static storage duration, and internal linkage all at once.

A local static instead has block scope and no linkage at all — there is nothing for another declaration to link to, because the name count was never visible outside function in the first place. Only the storage duration (static) is shared between the two examples.

So static local and file-scope static are really answering different combinations of the same three questions: a static local keeps a narrow, block-restricted scope while gaining program-length lifetime and staying unlinked to anything else, while a file-scope static keeps its wide, file-spanning scope, gains that same program-length lifetime, and additionally becomes off-limits to every other translation unit.

static int counter;   // file scope, static duration, internal linkage
void function(void) {
    static int count;   // block scope, static duration, no linkage
}

extern: Referring to an Object Defined Elsewhere

extern is used to declare that an object or function is defined somewhere else — typically in a different translation unit — so that the current file can refer to it without redefining it.

File 1 provides the actual definition of system_state, which is what allocates storage for it. File 2's extern declaration tells its compiler that an object of that name and type exists elsewhere with external linkage, so code in file2.c can now refer to the very same object that file1.c defined.

There is only ever one system_state object here — the one file1.c defines. file2.c's extern line creates no storage of its own; it is purely a reference that lets the linker connect file2.c's uses of the name back to file1.c's definition.

/* file1.c */
int system_state = 10;

/* file2.c */
extern int system_state;

Declaration vs Definition

Understanding extern well requires keeping declaration and definition apart. A definition is what actually creates the object and reserves storage for it; a declaration merely tells the compiler that an object with that name and type exists, possibly somewhere else entirely.

int count = 10; in file1.c is the definition — it creates the one and only count object. extern int count; in file2.c is a declaration only; test in file2.c increments the same object file1.c defined, not a second, independent one.

extern generally means: an object of this name and type, with external linkage, is defined somewhere else in the program. It doesn't itself create or initialize anything — it only tells the compiler what to expect so the linker can wire the reference up to the real definition.

/* file1.c */
int count = 10;

/* file2.c */
extern int count;

void test(void) {
    count++;
}

extern Does Not Usually Create a New Object

Because extern is a declaration rather than a definition, writing it on its own never allocates storage for a new object.

This line does not create a separate count object. It only tells the compiler that a count of external linkage is defined somewhere else in the program — and a matching definition must exist somewhere by the time the program is linked, or the link step fails.

extern int count;

Functions and extern

Ordinary function declarations behave a little differently from ordinary object declarations: functions have external linkage by default (unless declared static), so extern on a function declaration is usually implicit rather than required.

This definition already has external linkage by default, which means display can be called from another translation unit as long as that file declares it appropriately first.

Both forms declare the same externally-linked function. Since external linkage is already the default for an ordinary function declaration, writing extern in front of it changes nothing in practice — plain void display(void); is normally sufficient.

void display(void) {
    /* ... */
}
extern void display(void);

// is normally equivalent to:

void display(void);

static Functions for File-Private Helpers

static isn't limited to variables — it can also be applied to a function at file scope, and it has the same effect there: the function gains internal linkage and becomes callable only from within its own translation unit.

helper has internal linkage, so only code inside module.c — here, public_function — can call it. Another source file that also happens to define a function named helper gets its own, completely unrelated function; there is no name clash and no way for another file to reach this particular helper.

static void helper(void) is the function-level analog of a static file-scope variable: it is exactly how a module hides implementation details it does not want the rest of the program depending on, while still exposing whichever functions are meant to form its public interface.

/* module.c */
static void helper(void) {
    /* internal implementation detail */
}

void public_function(void) {
    helper();
}

External vs Internal Linkage

Linkage determines whether identifiers declared in different scopes, or in different translation units altogether, can end up referring to the same entity. An identifier with external linkage can be reached from other translation units; one with internal linkage is confined to the translation unit that declares it.

int global_value; at file scope has external linkage — every translation unit that declares a matching extern int global_value; is talking about the very same object. static int private_value; at file scope has internal linkage instead — file2.c simply has no way to reach file1.c's private_value at all, even by name.

Translation Units

Linkage is defined in terms of translation units, so it helps to be precise about what one is: a translation unit is essentially a source file after preprocessing has finished, including everything pulled in from the headers it includes.

Each source file is normally compiled into its own translation unit, and its own object file, independently of the others. The linker's job is to combine those separately-compiled object files into one executable, resolving any external-linkage references between them along the way.

The Storage-Class Specifiers

Everything covered so far is expressed through a small, fixed set of keywords. The major C storage-class specifiers are auto, register, static, extern, and _Thread_local, and each one's exact effect depends on the context — block scope or file scope, variable or function — in which it is written.

register

The register keyword historically suggested to the compiler that a variable should be kept in a CPU register for fast access, rather than in ordinary memory.

Modern optimizing compilers make their own register-allocation decisions regardless of what a programmer writes, so register should not be treated as a command that forces any particular placement — it is, at best, a hint the compiler is free to ignore entirely.

It's a mistake to assume register forces register allocation. Optimizing compilers routinely make better register-allocation decisions on their own than a hand-placed register keyword would, which is why the keyword has fallen out of common use in modern C code.

A real language restriction, not just a stylistic warning, is that you cannot apply & to an object declared register. Because the implementation is free to keep such an object purely in a register, the language simply does not guarantee it has an address to take in the first place — so &x is rejected at compile time.

register int counter;
register int x = 10;
int *p = &x;    // illegal

_Thread_local

C11 introduced _Thread_local to name a fourth storage duration — thread storage duration — for objects that need one independent instance per thread rather than one shared instance for the whole program.

Declaring counter this way gives every thread that ever runs its own private copy of counter. This single-threaded example only exercises the syntax, but in a multithreaded program each thread's writes to counter would be completely invisible to every other thread.

The three threads never share the same counter instance — each one is reading and writing its own private object, which is exactly the point of thread storage duration.

_Thread_local is most useful in multithreaded programs where each thread needs its own persistent state — a per-thread error code or a per-thread running total, for example — without the threads stepping on each other's values the way they would with an ordinary shared static object.

_Thread_local int counter;

int main(void) {
    counter = 5;
    return counter;
}

Scope vs Storage Duration vs Linkage: The Full Picture

With all three properties introduced, it is worth lining up a few declarations side by side to see exactly where they agree and where they differ.

All three declarations share static storage duration, but no two of them share the same combination of scope and linkage — which is exactly why treating 'static' as a single, uniform property leads to confusion.

// at file scope:
static int count;
// scope: file, storage duration: static, linkage: internal

// inside a function:
void test(void) {
    static int count;
    // scope: block, storage duration: static, linkage: none
}

// at file scope:
int global;
// scope: file, storage duration: static, linkage: external

Default Zero-Initialization for Static-Duration Objects

Storage duration also decides what happens when a declaration provides no explicit initializer. An object with static storage duration that is not explicitly initialized is set to zero (or the null value appropriate to its type) automatically, before the program's own code ever starts running.

Neither declaration provides an initializer, but because both have static storage duration, each is guaranteed to start out at the zero value appropriate to its type — an int at 0, a float at 0.0f — with no explicit work required.

x here has automatic storage duration and no initializer, so it holds whatever bits were already sitting in that memory — an indeterminate value, not a guaranteed zero.

Reading an uninitialized automatic variable and assuming it will be zero is a genuine, common bug. Only static-duration objects get that default zero-initialization guarantee; an uninitialized automatic local can hold literally anything, and code that happens to see 0 during testing may see something else entirely once the surrounding stack memory has been used differently.

int global_count;      // initialized to 0
static float value;    // initialized to 0.0f
void test(void) {
    int x;
    printf("%d\n", x);  // indeterminate value
}

Static vs Automatic Initialization Compared

Placing an automatic and a static local variable in the same function makes the initialization difference easy to see directly.

a is automatic and uninitialized, so its printed value is unpredictable and depends on whatever happened to be left in that stack memory. b is static, so even with no explicit initializer it is guaranteed to have started at 0 before test was ever called.

The difference comes entirely from storage duration and the initialization rules attached to it, not from anything about the types int being used — the same contrast would hold for any type that supports zero-initialization.

#include <stdio.h>

void test(void) {
    int a;
    static int b;
    printf("%d\n", a);  // indeterminate value
    printf("%d\n", b);  // 0
}

A Complete Scope Example

Putting file scope, function-level scope, and a nested block together in one example makes the visibility rules easy to trace all at once.

x has file scope, so it's visible everywhere in this file, including deep inside the nested block. y has block scope tied to test's body, so it's visible throughout test but nowhere else. z has block scope tied only to the innermost block, so it disappears the moment that block's closing brace is reached — the line after it simply cannot refer to z.

#include <stdio.h>

int x = 10;

void test(void) {
    int y = 20;
    {
        int z = 30;
        printf("%d\n", x);
        printf("%d\n", y);
        printf("%d\n", z);
    }
    printf("%d\n", x);
    printf("%d\n", y);
    // z is not accessible here
}

int main(void) {
    test();
    return 0;
}

Choosing static for Module Encapsulation

One of the most practically useful applications of file-scope static is hiding a source file's implementation details from the rest of the program, exposing only the handful of functions meant to form its public interface.

internal_counter and reset_counter both have internal linkage, so no other translation unit can reach either one directly, no matter what names it happens to reuse. process and get_processed_count are the deliberately public interface — everything else stays a private implementation detail of this one file.

This pattern reduces accidental dependencies between different parts of a program: another file cannot reset internal_counter behind process's back, or read it directly and bypass whatever bookkeeping get_processed_count is meant to guarantee, simply because those names are not visible to it at all.

#include <stdio.h>

static int internal_counter;

static void reset_counter(void) {
    internal_counter = 0;
}

void process(void) {
    internal_counter++;
}

int get_processed_count(void) {
    return internal_counter;
}

int main(void) {
    reset_counter();
    process();
    process();
    printf("%d\n", get_processed_count());
    return 0;
}

extern Across Multiple Files: The config.h Pattern

A very common real-project structure pairs one .c file that defines a shared object with a header that declares it, so that any number of other source files can share that single object safely.

config.c provides the one and only definition of system_mode. config.h provides nothing but a declaration — extern int system_mode; — and any file that includes that header, such as main.c, can then read or modify the very same object config.c defined.

A related mistake is putting the definition itself in the header instead of the declaration — writing int counter; in config.h rather than extern int counter;. Because a header is textually copied into every file that includes it, that would create a separate counter definition in each translation unit, which typically fails to link (or, at best, produces several unrelated objects instead of one shared one). The header should hold extern int counter;, and exactly one .c file should hold the real definition int counter;.

/* config.c */
int system_mode = 1;

/* config.h */
#ifndef CONFIG_H
#define CONFIG_H

extern int system_mode;

#endif

/* main.c */
#include "config.h"

int main(void) {
    system_mode = 2;
    return 0;
}

Why Unrestricted Global Variables Are Risky

A plain file-scope global with external linkage can be read and modified by every function that can see it, which makes it easy for unrelated parts of a program to end up depending on each other through shared state that is never mentioned in any function's parameters.

Any function anywhere in the program that can see counter is free to change it. Nothing about function1's own code protects its increment from being wiped out by an unrelated call to function2 somewhere else entirely.

Restricting counter's linkage with static, and only exposing it through increment and get_counter, means every read and write is now funneled through code that this one file controls — no other translation unit can reach counter directly any more, no matter what it is named there.

This is the general shape of the fix whenever unrestricted globals become a problem: keep the underlying object's linkage as narrow as possible (or its scope as narrow as possible, for a plain local), and expose only the specific, controlled operations that are actually meant to be part of the interface.

int counter;

void function1(void) {
    counter++;
}

void function2(void) {
    counter = 0;
}
static int counter;

void increment(void) {
    counter++;
}

int get_counter(void) {
    return counter;
}

A Three-Question Mental Model for Every Declaration

Faced with an unfamiliar declaration, it helps to ask the same three questions every time, in the same order, rather than trying to reason about storage classes all at once.

Scope is a fact about where an identifier is visible in the source text. Storage duration is a fact about how long the underlying object exists while the program runs. Linkage is a fact about whether some other declaration, possibly in another translation unit entirely, is allowed to name that same object or function. Answering all three questions separately for any declaration — rather than reaching for one word like 'static' and assuming it settles everything — is what prevents the great majority of storage-class mistakes.

Summary Table: Scope, Storage Class, and Linkage at a Glance

Every declaration in a C program sits somewhere in this table: pick its scope, pick its storage duration, and pick its linkage, and its behavior with respect to visibility and lifetime is fully pinned down — no matter how many of the five storage-class specifiers happen to be involved.

What is the difference between scope and storage duration, using a static local variable as the example?

Scope is about where a name can be used in the source code; storage duration is about how long the object behind that name exists while the program runs. A static local variable has block scope (its name is only usable inside its function) but static storage duration (the object survives for the entire program), which is exactly why it keeps its value between separate calls.

Why does a static local variable retain its value across calls while an ordinary local variable does not?

An ordinary local variable has automatic storage duration, so a fresh object is created and (re)initialized every time its block is entered and destroyed when the block ends. A static local variable instead has static storage duration: it is created and initialized only once, and every later call finds the same surviving object rather than a new one.

What does static actually change when it is applied to a file-scope declaration, versus when it is applied to a variable declared inside a function?

At file scope, the object already has static storage duration by default, so adding static there instead restricts its linkage to internal, making it invisible to other translation units. Inside a function, an ordinary local already has block scope and no linkage to restrict, so adding static instead changes its storage duration from automatic to static, letting it persist across calls.

What is a translation unit, and why does it matter when reasoning about linkage?

A translation unit is a source file after preprocessing, including everything pulled in from its included headers. Linkage is defined in terms of translation units: external linkage lets an identifier refer to the same entity across separate translation units, while internal linkage confines it to the one translation unit that declared it, which is why the concept has to be defined before internal versus external linkage makes sense.

Why is it illegal to take the address of a variable declared with register, and how does that follow from what register is asking for?

register asks the implementation to consider keeping the object somewhere other than ordinary addressable memory, such as a CPU register, and the language does not guarantee such an object has a conventional address at all. Because & requires an addressable object, applying it to a register-declared variable is rejected at compile time rather than silently ignored.

Write a C program that demonstrates variable shadowing between a global variable and a local variable that share the same name.

There are two separate objects named value here: the file-scope global holding 100, and main's own local holding 5. Inside main, the local declaration is the nearer one, so it shadows the global and the program prints 5, even though the global value still exists unchanged at file scope.

Write a C program with a function counter() that uses a static local variable to track and print how many times it has been called, across three separate calls from main.

count is declared static, so it is created and initialized to 0 only once, the first time counter runs; every later call finds the same surviving object instead of a fresh one. That is why the three calls print 1, 2, and 3 rather than 1, 1, and 1.

Write a C program that defines two versions of a call counter — one using an automatic local variable and one using a static local variable — and calls each three times to show that only the static version accumulates its count.

automaticCounter's count has automatic storage duration, so it is recreated and reset to 0 on every call, printing automatic: 1 all three times. staticCounter's count has static storage duration, so it survives between calls and prints static: 1, static: 2, and static: 3 — the only difference between the two functions is that one keyword, static.

Write a C program that uses file-scope static to hide a helper function and an internal counter from outside the file, exposing only a public function and an accessor function.

internal_counter and reset_counter are declared static at file scope, giving them internal linkage, so no other translation unit could ever call reset_counter or touch internal_counter directly, even if it declared identically-named entities of its own. process and get_processed_count are left with ordinary external linkage as this file's intended public interface, and the program prints 3 after three calls to process.

Write a C program with a global counter and two functions, one that increments it and one that resets it, then print the counter's final value from main.

counter has file scope and external linkage, so both function1 and function2 read and write the exact same object with no parameters involved. Two calls to function1 bring it to 2, function2 resets it to 0, and one more call to function1 leaves it at 1, which is what main prints — illustrating how easily unrestricted shared state can be changed from more than one place.

Write a C program that uses a goto statement and a label to demonstrate function scope.

The label positive: has function scope, meaning it can be referenced by any goto within test regardless of textual order. Calling test(5) jumps straight to the label and prints Positive; calling test(-3) never executes the goto and instead falls through to print Negative or zero.

Write a C program that declares a _Thread_local variable, assigns it a value, and prints it from main.

thread_counter is declared with _Thread_local, giving it thread storage duration: in a multithreaded program, each thread that touches it would get its own independent instance rather than sharing one object. Run on a single thread, as here, it simply behaves like an ordinary static-duration object, printing 42.

Write a C program that uses the register storage-class specifier on a loop-control variable and prints its final value, without ever taking that variable's address.

register only suggests that i is a good candidate for fast, register-style access; the compiler is free to honor or ignore that suggestion. The program never writes &i anywhere, which is required, since taking the address of a register-declared object is illegal precisely because the language does not guarantee it occupies addressable memory at all.

Write a C program that compares an uninitialized automatic local variable with a static local variable that has no explicit initializer, printing both.

a has automatic storage duration and no initializer, so it holds whatever bits already happened to occupy that stack memory — an indeterminate value that should never be relied on. b has static storage duration, so even without an explicit initializer the language guarantees it starts out zero, which is why printing b is always safe in a way that printing a is not.

Write a C program that declares an extern reference to a global variable which is defined later in the same file, and modifies that variable through a function, to illustrate the difference between a declaration and a definition.

The extern int shared_count; line near the top is only a declaration, telling the compiler that a shared_count of external linkage exists somewhere in the program; it creates no storage of its own. The actual definition, int shared_count = 0;, appears later in the same file and is what allocates the one real object that increment and main both end up sharing, so the program prints 2.