Skip to content
C++ Better Explained
Go back
Storage Classes in C++: static, extern, thread_local, and mutable

Storage Classes in C++: static, extern, thread_local, and mutable

Every variable in your program answers three questions: when is it created and destroyed?, who can see it?, and can the linker match it across files? Storage class specifiers are how you change the default answers.

There are only four in modern C++, and most beginner confusion comes from static, which unhelpfully means two different things depending on where you write it.


The Default: Automatic Storage

If you write no specifier at all, a local variable has automatic storage duration:

#include <iostream>

void counter() {
    int count = 0;      // created on entry, destroyed on exit
    count++;
    std::cout << count << " ";
}

int main() {
    counter();
    counter();
    counter();          // prints: 1 1 1
}

count is born when the function starts and dies when it returns, every single time. That’s why the output is 1 1 1 and not 1 2 3.

Note on auto: In C++98, auto was the keyword for exactly this default — which made it pointless to type. C++11 recycled it for type deduction, so auto x = 5; now means “figure out the type,” not “give this automatic storage.”


static Inside a Function: Persistent Locals

Add static to that same local and its lifetime stretches to the whole program, while its visibility stays inside the function:

#include <iostream>

void counter() {
    static int count = 0;   // initialised once, on first call
    count++;
    std::cout << count << " ";
}

int main() {
    counter();
    counter();
    counter();              // prints: 1 2 3
}

The initialiser runs exactly once, on the first call. After that the variable persists, holding its value between calls — but no other function can touch it, because the name is still local. It’s a private, permanent scratchpad.

If you're looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you're a complete beginner or looking to solidify your understanding. Just $19.

static at File Scope: Internal Linkage

Write static on a global variable or function and it means something different — it makes the name private to that .cpp file:

// logger.cpp
static int messageCount = 0;      // only logger.cpp can see this

static void writeToDisk() { }     // only logger.cpp can call this

Another source file can now declare its own messageCount without a clash, because the linker never sees these names at all. This is how you keep implementation details out of the global namespace.

(An unnamed namespace does the same job and is generally preferred in modern C++, but static still works and you’ll see it everywhere.)


extern: Sharing One Variable Across Files

extern is the mirror image of file-scope static. It says “this exists, but it’s defined somewhere else — linker, go find it.”

You define it in exactly one source file:

// config.cpp
int maxUsers = 100;               // the one real definition

And declare it in a header everyone includes:

// config.h
#pragma once
extern int maxUsers;              // declaration only, no storage
// main.cpp
#include <iostream>
#include "config.h"

int main() {
    std::cout << "Max users: " << maxUsers << "\n";
}

Compile both together and it links:

g++ main.cpp config.cpp -o app

The rule to remember: extern declares, no extern defines. Drop the extern in the header and every including file creates its own maxUsers, and you’re back to a multiple-definition linker error.


thread_local: One Copy Per Thread

thread_local gives each thread its own independent copy of a variable, created when the thread starts and destroyed when it ends:

#include <iostream>
#include <thread>

thread_local int localId = 0;

void work(int id) {
    localId = id;                 // touches only this thread's copy
    std::cout << "Thread " << localId << "\n";
}

int main() {
    std::thread t1(work, 1);
    std::thread t2(work, 2);
    t1.join();
    t2.join();
}

Both threads write to localId and neither interferes with the other — no mutex needed, because there is no shared state. It’s useful for per-thread caches, random number generator state, and error codes.


mutable: The Escape Hatch for const

mutable is the odd one out — it applies only to non-static class members, and it lets them change even through a const object:

#include <iostream>
#include <string>
#include <utility>

class Document {
    std::string text;
    mutable int readCount = 0;    // changeable even in const methods

public:
    Document(std::string t) : text(std::move(t)) {}

    const std::string& read() const {
        ++readCount;              // legal only because of mutable
        return text;
    }

    int reads() const { return readCount; }
};

int main() {
    const Document doc("hello");
    doc.read();
    doc.read();
    std::cout << doc.reads() << "\n";   // 2
}

read() is const because it doesn’t change what the document means — but it does update bookkeeping. mutable is for exactly that: caches, hit counters, and lazily computed values that don’t affect the object’s logical state.


Quick Reference

SpecifierLifetimeVisibilityTypical use
(none)function calllocal blockordinary locals
static (local)whole programthat functioncounters, one-time setup
static (file scope)whole programthat .cpp fileprivate helpers
externwhole programevery file that declares itshared config
thread_localthread lifetimeper threadper-thread state
mutablewith the objectclass membercaches in const methods


Take Your C++ Further

If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.

👉 Get the C++ Better Explained Ebook — $19

📋

Free Download: The 10 Mistakes Every C++ Beginner Makes

A free 1-page checklist that shows the exact traps that slow down every C++ beginner — so you can avoid them from day one.

🔒 No spam. Unsubscribe anytime.


Share this post on:

Written by

Sahil Bora

Software Engineer. Author and creator of C++ Better Explained.


Previous Post
Matrix Multiplication in C++: Full Program Explained Step by Step
Next Post
Union in C++: What It Is and When You Should Use One

Keep Learning