C++ / CLASSES AND OBJECT LIFECYCLE
Static members shared across all objects
Share one piece of state across every object of a class using static data members and static member functions, and keep it correct as objects are copied.
What you will learn
- Declare a static member in the class and define it once outside, or use inline static
- Expose shared state through static member functions callable as Class::fn()
- Keep counters honest by updating them in copy and move constructors as well
- Read "undefined reference to Class::member" as a missing static definition
Understanding Static members shared across all objects
A non-static data member exists once per object, and its address is computed from this. A static data member is the opposite: the line inside the class says only that the member exists, while the storage is a single object with the lifetime of the program that the class name scopes and that access specifiers still guard. Because that storage is not inside any instance, sizeof the class does not grow when you add one, and the value can be read before the first object is constructed and after the last one is destroyed.
Since C++17 you can write inline static int n = 0; or static constexpr int n = 16; inside the class and be finished. Before that, the in-class line is a declaration only, and you must supply exactly one definition at namespace scope, int Session::liveCount = 0;, in a single .cpp file. Omit it and every translation unit still compiles cleanly, because the compiler only needed the declaration; the problem appears at link time as an undefined reference to the member.
Static member functions follow the same reasoning one level up: they receive no this, so they cannot read non-static members, cannot be marked const because there is no object to protect, and cannot be virtual because there is no object to dispatch on. They are still members, so they can reach private names, which makes them the natural place to publish or guard shared state. Keeping the shared value truthful is your responsibility: a counter incremented in one constructor but not in the copy or move constructor will drift downward, because every object that dies runs a destructor and decrements it exactly once.
<iostream>
class Session {
public:
Session() { ++liveCount; ++totalCreated; }
~Session() { --liveCount; }
static int live() { return liveCount; }
static int total() { return totalCreated; }
private:
static int liveCount; // declaration only
static int totalCreated; // declaration only
};
int Session::liveCount = 0; // the one and only storage
int Session::totalCreated = 0;
int main() {
std::cout << Session::live() << " " << Session::total() << "\n";
{
Session a, b;
std::cout << Session::live() << " " << Session::total() << "\n";
Session c;
std::cout << a.live() << " " << c.total() << "\n";
}
std::cout << Session::live() << " " << Session::total() << "\n";
}
A static member belongs to the class rather than to any object, so exactly one of it exists for the whole program even when zero objects do.
Worked examples
Header-friendly statics with inline and constexpr
Shows the C++17 forms that need no out-of-class definition, and proves every object sees the same storage.
<iostream>
struct Grid {
static constexpr int cellSize = 16; // implicitly inline
inline static int instances = 0; // defined right here
Grid() { ++instances; }
};
int main() {
Grid g1, g2;
std::cout << Grid::cellSize * 3 << "\n";
std::cout << Grid::instances << "\n";
std::cout << (&g1.instances == &g2.instances) << "\n";
std::cout << (&Grid::instances == &g1.instances) << "\n";
}
Example explained
Line 1static constexpr int cellSize = 16; is implicitly inline in C++17, so the constant can sit in a header with no companion .cpp line.
Line 2inline static int instances = 0; is a definition, not just a declaration, which is why no int Grid::instances line is needed below the class.
Line 3&g1.instances == &g2.instances is true because the g1. and g2. parts are evaluated then discarded; both expressions name the single shared int.
Line 4&Grid::instances has type int*, not a pointer-to-member, another sign the member is not laid out inside a Grid.
Static factory with a private constructor
Uses a static member function to hand out objects and a static counter to number them, with no object needed to make the first call.
<iostream>
<string>
class Ticket {
public:
static Ticket issue(const std::string& holder) {
return Ticket(holder, nextSerial++);
}
static int issued() { return nextSerial - 1000; }
void print() const { std::cout << serial << ":" << holder << "\n"; }
private:
Ticket(const std::string& h, int s) : holder(h), serial(s) {}
std::string holder;
int serial;
static int nextSerial;
};
int Ticket::nextSerial = 1000;
int main() {
Ticket a = Ticket::issue("ada");
Ticket b = Ticket::issue("linus");
a.print();
b.print();
std::cout << Ticket::issued() << "\n";
}
Example explained
Line 1Ticket::issue is called when no Ticket exists yet, which is only possible because a static member function needs no receiver.
Line 2issue can call the private constructor: access control depends on being a member of the class, not on having a this pointer.
Line 3nextSerial++ yields the old value, so the first ticket gets 1000 and the shared counter is left at 1001 for the next caller.
Line 4issued() answers from the same single counter, so it needs no Ticket to inspect and stays correct as tickets are destroyed.
Important notes
A static data member is not thread-safe on its own; two threads constructing objects at once race on ++count, so use std::atomic<int> or a mutex if that can happen.
Static data members are initialised before main and destroyed after it, so a static object in one translation unit must not depend on a static in another during its own initialisation.
Common mistakes
Writing static int count; in the class and nothing else: every file compiles, then the linker fails with "undefined reference to Class::count", which looks unrelated to the class definition.
Reading a non-static member from a static member function, for example return name.size(); inside a static function, which fails to compile because there is no this to find name in.
Incrementing the counter only in a hand-written constructor while the compiler-generated copy constructor makes extra objects: each copy's destructor still decrements, so the live count drifts and can go negative.
Repeating the keyword in the out-of-class definition as static int Class::count = 0;, which is ill-formed; static appears only in the in-class declaration.
Try it yourself
Change, predict, then run
Add a private static int peak to the Session class that records the largest value liveCount ever reached, update it in the constructor, and print Session::peak() after all sessions have been destroyed.
Open the C++ workspaceCheck your understanding
A class increments a static count in its default constructor and decrements it in its destructor, and declares no copy constructor. You pass an existing named object of that class to a function by value. What is count after the call returns?
- One lower than before the call, because the parameter's destructor ran but no constructor incremented for it
- Unchanged, because the compiler-generated copy constructor also increments count
- One higher than before the call, because passing by value created an extra object
- Unchanged, because destructors do not run for parameters passed by value
Show answer
The implicitly generated copy constructor only copy-initialises the members; it knows nothing about count, so creating the parameter adds nothing. The parameter is still a real object, so its destructor runs when the function returns and decrements the shared counter once. Option two is the tempting one: it assumes the generated copy constructor mirrors the body of the default constructor, which it never does. Fixing this means writing a copy constructor that increments too.