Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

C++ Class Member Variables: Should You Declare in Headers?

Learn if you should declare or initialize C++ class members in header files. Clarify pointer vs reference usage in modern C++.
Split screen showing messy and clean C++ header files to highlight best practices in class member declarations Split screen showing messy and clean C++ header files to highlight best practices in class member declarations
  • ⚙️ Defining non-inline static variables in headers breaks the One Definition Rule.
  • 🧠 Using forward declarations makes compile times faster and reduces how much headers rely on each other.
  • 💥 Public member variables break encapsulation and weaken maintainability.
  • 🧹 Default member initializers are efficient but best used for simple types or constants.
  • 🚀 Smart pointers like std::unique_ptr ensure safe, automatic memory management in class members.

When you build good C++ programs, how you set up your class variables can affect everything. This includes how fast your code compiles and how easy it is to maintain for a long time. This article will go over the main ideas, choices, and good ways to organize class members and header files in modern C++. This helps create object-oriented designs that can grow and stay clean.


What Are C++ Class Member Variables and Header Files?

In C++, a class member variable is a data member that is part of a class object. Each object of that class type has its own copy of its non-static member variables. These variables show the specific state of each object.

For example:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

class Car {
private:
    int speed;  // class member variable
public:
    void accelerate();
};

Here, every Car instance stores its own speed value.

C++ programming usually splits code into header files (.h / .hpp) and source files (.cpp):

  • Header files: Describe what a class is — its structure, member variables, and function declarations.
  • Source files: Define how the class behaves—the implementation of the member functions.

This makes code easier to read, helps organize projects, and allows for building parts of the code separately. It also helps code work with other compiled code. And then, it makes recompiling faster when only the implementation (not the interface) changes.

Moreover, class definitions use access rules:

  • public: Accessible outside the class.
  • private: Restricted to the class itself.
  • protected: Accessible to the class and its derived classes.

Starting from a clear way of separating parts sets up C++ codebases that can grow and follow rules.


Where Should You Declare Class Member Variables?

In C++, class member variables must be declared in the class definition. This is usually in a header file. This lets the compiler see what data members the class has.

Example:

// Car.h
class Car {
private:
    int speed; // Declaration only
};

This gives the necessary type information to other parts of the program using or making objects of the class.

Static Members: A Two-Location Requirement

For static member variables, both declaration and definition are required:

  • Declaration in the class (usually in the header file),
  • Definition in exactly one source file.
// Car.h
class Car {
public:
    static int totalCars;
};

// Car.cpp
int Car::totalCars = 0;

Failing to define static variables outside the class causes linker errors. Defining them in more than one compilation unit (for example, putting the definition in a header that gets included in many places) breaks the One Definition Rule (ODR). This can lead to hard-to-figure-out linker failures.


Variable Initialization: Should It Be in the Header?

C++11 introduced a useful feature called in-class member initializers. This lets you set a default value for a member variable at the point of declaration.

class Car {
private:
    int speed = 0;
};

People suggest doing this for constants and simple types because:

  • It makes constructors shorter.
  • It makes code easier to read.
  • It works well with constructors that don’t give other starting values.

But there are important things to watch out for when initializing in headers:

  1. Do not put complex logic inside initializers.

    • Simple is fine (for example, speed = 0).
    • Complex causes problems (for example, using results of function calls or information that changes based on the system).
  2. Do not initialize non-inline static data members in headers.

    • They will be created many times if headers are included in different translation units.
    • This breaks the One Definition Rule.
  3. Header files are included by many .cpp files, so anything defined in them might be copied unless it’s inline.

When you are not sure, initialize simple constants in the header. Let constructors defined in .cpp files handle complex setup.


The Compiler’s Perspective: Header File Bloat and Redundancy

To the compiler, header files are like copy-pasted into places where they are included. So:

  • Every .cpp file that #includes a header will compile all its contents again and again.
  • Putting heavy logic, large initializations, or complex definitions in headers makes compile times slower, makes the compiled program bigger, and can cause name clashes.

🧠 As Herb Sutter notes in C++ Coding Standards (2004):

"Every line in a header gets compiled as many times as the number of inclusions."

That means if 20 translation units include a header with too much stuff, it may be compiled 20 times.

To fix this:

  • Put only the absolute necessary content in headers.
  • Use forward declarations instead of #include whenever you can (we will talk more about this below).
  • Put logic and large object definitions in .cpp files.

Pointers vs. References: Declaring Smartly in Class Members

How you declare object relationships in class member variables affects how flexible things are, how long objects live, and how well the code runs.

Let’s look at your three common choices:

1. Raw Pointers (Type*)

✅ Use when optional control or initialization is needed.
❗ Must be set up by hand.
❗ Can lead to pointers pointing to nothing or memory that no one controls.

Example:

class Car {
private:
    Engine* engine;
public:
    Car() { engine = new Engine(); }
};

But raw pointers need you to manually delete them, which is easy to make mistakes with:

~Car() { delete engine; }  // Unsafe if exception occurs before

2. References (Type&)

✅ Must be initialized at object creation.
🚫 Cannot point to null.
🚫 Cannot be reseated.

References are strong, but they are not very flexible. They are good if the referenced object will always last longer than the class itself.

3. Smart Pointers (std::unique_ptr, std::shared_ptr)

🌟 This is the suggested modern way to do things.
🌟 They make it clear who owns the memory and handle memory automatically.
🌟 They stop memory leaks and pointers from pointing to nothing.

#include <memory>

class Engine;

class Car {
private:
    std::unique_ptr<Engine> engine;
public:
    Car() : engine(std::make_unique<Engine>()) {}
};

Use std::unique_ptr when an object is the only one controlling the resource.
Use std::shared_ptr when ownership is shared or there are complex links between parts (for example, in programs that handle how parts depend on each other).


Forward Declarations in Headers: Minimize Coupling

Every time you include a header using #include, you bring in all the declarations, dependencies, and possibly other includes that come along with it.

Do not include too much. Use forward declarations.

When You Can Forward Declare:

  • When using pointers or references to a class type:
class Engine;  // forward declaration

class Car {
private:
    Engine* engine;
};

When You Cannot:

  • When using the class by value — the compiler must know its full size:
class Car {
private:
    Engine engine; // Requires full class definition of Engine
};
  • When calling member functions of the referenced class within the header.

  • When dealing with base classes for inheritance where objects can take on many forms (because of vtables).

Minimizing includes makes the build system faster. At the same time, it keeps classes separate.


Static Class Members: Defining Once and Only Once

A static member variable exists once per class, not per instance. Use them for:

  • State shared by all objects (for example, a global counter or logging level).
  • Constants (for example, default settings).

🚫 Static variables SHOULD NOT be defined in header files unless:

  • They're declared inline static (available since C++17).
  • They're constant expressions (constexpr).

Traditional Example:

// Logger.h
class Logger {
public:
    static int logLevel;
};

// Logger.cpp
int Logger::logLevel = 2;

Modern, safer approach with C++17:

// Logger.h
class Logger {
public:
    inline static int logLevel = 2;
    static constexpr int DefaultLogLevel = 2;
};

Benefits of inline static:

  • Allows definition in header files.
  • The compiler makes sure it is only created once.
  • Very good for header-only tools or templates.

Inline Variables and Modern C++ Practices (C++17+)

C++17 brought in inline variables to fix a long-standing problem with static member variables.

🔧 A variable declared as inline makes sure that only one definition will exist, even if many source files include it.

This is a big change for header-only libraries or heavily templated classes.

Example of modern design:

#include <string>

class Config {
public:
    inline static const std::string AppName = "DevsolusDebugger";
    static constexpr int MaxConnections = 10;
};

These variables can now be used safely and included across many files without breaking ODR rules.

But note this:

  • Avoid values that change during the program's run and hold a state with inline static.
  • Use them for constant settings or values known when the code is compiled.

Visibility: Keep Member Data Private or Protected

Limiting access to member variables is a rule in almost every object-oriented programming language. C++ also follows this.

✅ Keep your class member variables private or protected.
🚫 Avoid making them public unless justified (like POD structs).

This helps encapsulation, a very important rule of the SOLID design model.

class Car {
private:
    int speed;
public:
    void setSpeed(int s) { speed = s; }
    int getSpeed() const { return speed; }
};

Benefits:

  • Stops accidental or unwanted changes.
  • Lets you check data or record things within setters.
  • You can change how things work inside the class without changing how others use it.

Making member variables public shows how your code works and makes it less flexible later on.


Real-World Example: Designing a Scalable Class Interface

Let’s build a cleaner class that uses all these good ways of doing things.

Header: Car.h

#pragma once
#include <memory>

class Engine;  // Forward declaration

class Car {
public:
    Car();
    void drive();

private:
    std::unique_ptr<Engine> engine;
    int speed = 0;
};

Source: Car.cpp

#include "Car.h"
#include "Engine.h"

Car::Car() : engine(std::make_unique<Engine>()) {}

void Car::drive() {
    // Driving logic here
}

✅ Minimal includes
✅ Smart pointers make it clear who owns things.
✅ Simple way to set up variables.
✅ Compiles fast.


Common Pitfalls & Anti-Patterns

Avoid these mistakes in C++ class design:

  • ❌ Defining non-inline static members in header files.
  • ❌ Including unnecessary headers, which makes too many parts rely on each other.
  • ❌ Using raw pointers without clear ownership or delete.
  • ❌ Exposing public data members, which breaks encapsulation.
  • ❌ Repeating variable initializations in many constructors.
  • ❌ Using in-class initializations for data that changes when the program runs.

Each mistake can cause hard-to-find bugs, make it tough to maintain, or slow down your code. This is especially true in large projects with many parts.


Best Practices Checklist

Here's a quick-reference design checklist:

✅ Declare data members in headers; define them in .cpp files as needed.
✅ Use unique_ptr / shared_ptr to show who owns memory.
✅ Prefer forward declarations when possible.
✅ Avoid #include in headers unless really needed.
✅ Use inline static or constexpr for static members (C++17+).
✅ Never define static non-inline variables directly inside headers.
✅ Always keep data members private / protected.
✅ Keep headers small. Move logic to implementation files.
✅ Use default member initializers (safely) to keep things simple.
✅ Follow the One Definition Rule and put definitions in their own place.


Making Informed Decisions in Your C++ Design

Good object-oriented design in C++ does more than just work today. It plans for how things might get complex, how people will work together, and for future changes. The way you declare class member variables and use header files affects everything from how well the compiler works to how fast your team gets things done. When you use encapsulation, follow ODR, and make use of tools like smart pointers and forward declarations, your classes become well-organized, long-lasting parts.

These good practices are not just about style. They are smart ways to make code easier to read, maintain, and perform well. Build with a clear purpose.


Citations

Stroustrup, B. (2013). The C++ Programming Language (4th ed.). Addison-Wesley.

Meyers, S. (2005). Effective C++: 55 Specific Ways to Improve Your Programs and Designs (3rd ed.). Addison-Wesley.

Sutter, H. (2004). C++ Coding Standards: 101 Rules, Guidelines, and Best Practices. Pearson Education.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading