Skip to main content
C++ · lesson 24 of 24
C++ intermediate Lesson 24 of 24

Static Members in C++

Learn static data members and static member functions in C++ — shared per-class state, instance counters, and factory functions that run without an object.

By SkillByExample · Published

Static Members in C++

A static data member belongs to the class itself, not to any one object — every instance shares the same copy. A static member function goes further: it has no this pointer at all, so it can be called through the class name without an object existing. Both are the mechanism for state and behavior that’s per-class rather than per-instance.

Static Data Members — Shared State

Declaring a member static inside the class only reserves its name; it still needs a definition elsewhere that gives it storage and an initial value.

#include <iostream>

class Widget {
    static int liveCount_;   // declaration only — shared by every Widget
public:
    Widget()  { ++liveCount_; }
    ~Widget() { --liveCount_; }

    static int liveCount() { return liveCount_; }  // no *this — reads shared state only
};

// One definition, outside the class, gives liveCount_ storage and its initial value
int Widget::liveCount_ = 0;

int main() {
    std::cout << "before: " << Widget::liveCount() << "\n";

    Widget a, b;
    std::cout << "after a, b: " << Widget::liveCount() << "\n";

    {
        Widget c;
        std::cout << "after c: " << Widget::liveCount() << "\n";
    } // c destroyed here

    std::cout << "after c's scope ends: " << Widget::liveCount() << "\n";
}
$ g++ -std=c++17 -Wall -Wextra -o counter counter.cpp
$ ./counter
before: 0
after a, b: 2
after c: 3
after c's scope ends: 2

liveCount_ reaches 0 before any Widget exists, climbs as constructors run, and drops as destructors run when c goes out of scope — one counter, tracking every object of the class at once, with no object needed to read it (Widget::liveCount(), not a.liveCount()).

Static Member Functions as Factories

Because a static member function has no this, it can’t touch liveCount_’s non-static cousins on some particular object — but it can call a private constructor, which makes it useful for enforcing invariants before an object is ever built.

#include <iostream>
#include <string>

class Account {
    static constexpr double MIN_BALANCE = 10.0;  // shared constant — no storage per object
    std::string owner_;
    double balance_;

    // Private constructor: the only way to build one is through the static factory below
    Account(std::string owner, double balance) : owner_(std::move(owner)), balance_(balance) {}

public:
    // Static member function: called via Account::open(...), not on an instance
    static Account open(const std::string& owner, double deposit) {
        if (deposit < MIN_BALANCE)
            deposit = MIN_BALANCE;  // enforce the invariant before construction
        return Account(owner, deposit);
    }

    void print() const {
        std::cout << owner_ << ": $" << balance_ << "\n";
    }
};

int main() {
    Account a = Account::open("Priya", 50.0);
    Account b = Account::open("Sam", 2.0);   // below minimum — factory bumps it up

    a.print();
    b.print();
}
$ g++ -std=c++17 -Wall -Wextra -o factory factory.cpp
$ ./factory
Priya: $50
Sam: $10

Account’s constructor is private, so Account("Sam", 2.0) won’t compile from main — the only entry point is Account::open, which can call the private constructor because it’s a member of the same class. MIN_BALANCE needs no per-object storage either: static constexpr makes it a single compile-time constant shared by the class, not copied into every Account.

Practice

  1. Add a static int totalDeposited_ to Account that accumulates every deposit passed to open, plus a static double totalDeposited() accessor. Print the total after creating a few accounts.

    Solution
    static double totalDeposited_;
    static double totalDeposited() { return totalDeposited_; }
    
    static Account open(const std::string& owner, double deposit) {
        if (deposit < MIN_BALANCE) deposit = MIN_BALANCE;
        totalDeposited_ += deposit;
        return Account(owner, deposit);
    }
    // outside the class:
    double Account::totalDeposited_ = 0.0;
  2. Remove the out-of-class definition int Widget::liveCount_ = 0; and try to compile. Explain the linker error you get.

    Solution

    The linker reports an undefined reference to Widget::liveCount_. The in-class line only declares the member; without a definition it has no storage, so nothing exists for ++liveCount_ to increment.

  3. Change Widget::liveCount() to a non-static member function (drop static, keep the body). Try calling Widget::liveCount() the same way as before. What error do you get, and why?

    Solution

    A compile error: a non-static member function requires an object (a.liveCount()), because it implicitly takes this. Widget::liveCount() with no object only works for static members, which don’t need one.

  4. Add a static member function Widget::reset() that sets liveCount_ back to 0, and call it after the destructors have already run in main. Does it compile even though no Widget exists at that point?

    Solution

    Yes — static int reset() { liveCount_ = 0; return liveCount_; } compiles and runs fine with zero live objects, because static members belong to the class, not to any instance.

  5. Make MIN_BALANCE a plain (non-constexpr) static const double instead, and remove the in-class initializer, adding double Account::MIN_BALANCE = 10.0; outside the class instead. Confirm it still compiles and behaves the same.

    Solution
    static const double MIN_BALANCE;   // declaration only, no initializer in-class
    // outside the class:
    const double Account::MIN_BALANCE = 10.0;

    This is the pre-C++17 pattern static constexpr was designed to avoid — it still works, but needs the same out-of-class definition every other static data member needs.

Frequently Asked Questions

What's the difference between a static data member and a regular member?
A regular member has one copy per object; a static member has exactly one copy shared by every object of the class, and it exists even if no objects have been created yet.
Why do I need a separate definition line outside the class for a static data member?
The in-class line only declares the static member — it doesn't give it storage. Before C++17's `inline` specifier, exactly one translation unit had to provide a definition (`Type ClassName::member = value;`) or the linker would fail with an undefined reference.
Can a static member function access non-static members?
No. A static member function has no `this` pointer, so it can only touch other static members or an object passed to it explicitly — it isn't tied to any particular instance.
When should I use a static member function instead of a free function?
When the function needs private access to the class (like a factory that calls a private constructor) or belongs conceptually to the class's namespace, but doesn't need an existing object to operate on.
What does `static constexpr` add over plain `static const`?
`static constexpr` guarantees the value is a compile-time constant and, since C++17, is implicitly `inline` — so it doesn't require an out-of-class definition even if something takes its address.