Organizing Class Definitions
Split class definitions between header and source files for better compilation.
What Are Classes and Header Files?
Everything a class needs in order to be used splits into two halves. There is the class definition, which lists the members, their types, and their access levels, and there are the member function definitions, which contain the code those functions run. C++ lets you place those two halves in different files, and where you put each one decides who can use the class, whether the linker complains, and how much of your project has to be rebuilt when you change a line.
This lesson works through that placement decision using one small class throughout: a Bookmark that remembers where a reader stopped.
Written the way you have written classes so far, everything lives in one place:
#include <iostream>
class Bookmark
{
private:
int m_page{};
int m_line{};
public:
Bookmark(int page, int line)
: m_page{ page }
, m_line{ line }
{
}
void describe() const
{
std::cout << "resuming at page " << m_page << ", line " << m_line << '\n';
}
void advance(int pages)
{
m_page += pages;
m_line = 1;
}
int getPage() const { return m_page; }
int getLine() const { return m_line; }
};
int main()
{
Bookmark resume{ 118, 7 };
resume.describe();
resume.advance(3);
resume.describe();
return 0;
}
resuming at page 118, line 7
resuming at page 121, line 1
This is fine at four members. At forty, a reader who only wants to know what Bookmark can do has to scroll past every line of code that makes it work. Someone using a class needs its public interface, not its implementation, and the two are tangled together here.
One Class, Four Homes For A Definition
A member function definition can sit in one of four places, and each combination has different consequences. This table is the map for the rest of the lesson:
| Where the definition sits | Implicitly inline? | Usable from other .cpp files? | A change rebuilds |
|---|---|---|---|
| Inside the class body, class in a .cpp file | yes | no, the class is private to that file | that one .cpp file |
| Inside the class body, class in a header | yes | yes | every .cpp file that includes the header |
Outside the class body, in a header, marked inline |
no, you write inline yourself |
yes | every .cpp file that includes the header |
| Outside the class body, in a .cpp file | no | yes, once that .cpp is linked in | that one .cpp file |
Two questions run through the whole table. Is the definition visible to a translation unit that wants to call the function, and does it break the one definition rule if more than one translation unit sees it? The rest of this lesson answers both.
Moving A Definition Out Of The Class Body
The declaration of a member function has to stay inside the class definition, because it is part of what the type is. The body can be lifted out. When you do that, prefix the function name with the class name and the scope resolution operator so the compiler knows you are defining a member rather than an unrelated free function:
#include <iostream>
class Bookmark
{
private:
int m_page{};
int m_line{};
public:
Bookmark(int page, int line);
void describe() const;
void advance(int pages);
int getPage() const { return m_page; }
int getLine() const { return m_line; }
};
Bookmark::Bookmark(int page, int line)
: m_page{ page }
, m_line{ line }
{
}
void Bookmark::describe() const
{
std::cout << "resuming at page " << m_page << ", line " << m_line << '\n';
}
void Bookmark::advance(int pages)
{
m_page += pages;
m_line = 1;
}
int main()
{
Bookmark resume{ 118, 7 };
resume.describe();
resume.advance(3);
resume.describe();
return 0;
}
resuming at page 118, line 7
resuming at page 121, line 1
The program behaves identically, but the class definition now reads as a summary: two data members, a constructor, and three things a bookmark can do. Note that const still belongs on describe() in both the declaration and the definition, and that Bookmark:: appears once, in front of the function name, not in front of the return type.
getPage() and getLine() stayed where they were. Hauling a one-line accessor out of the class costs three lines and a repeated signature to save one line of clutter, which is a bad trade. Trivial functions are usually left in the class body.
Giving The Class Its Own Pair Of Files
A class defined inside a .cpp file can only be used by that .cpp file. To share it, put the class definition in a header and the non-trivial member functions in a matching source file. Both files are conventionally named after the class.
Unlike a function, which can be called given only a forward declaration, a type usually cannot be used from a forward declaration alone. The compiler has to see the complete class definition, for two reasons: it needs the member declarations to check that every use of the type is legal, and it needs to know how large an object of the type is before it can create one. So the header carries the full class definition, not a stub.
Bookmark.h:
#pragma once
class Bookmark
{
private:
int m_page{};
int m_line{};
public:
Bookmark(int page, int line);
void describe() const;
void advance(int pages = 1);
int getPage() const { return m_page; }
int getLine() const { return m_line; }
};
Bookmark.cpp:
#include "Bookmark.h"
#include <iostream>
Bookmark::Bookmark(int page, int line)
: m_page{ page }
, m_line{ line }
{
}
void Bookmark::describe() const
{
std::cout << "resuming at page " << m_page << ", line " << m_line << '\n';
}
void Bookmark::advance(int pages)
{
m_page += pages;
m_line = 1;
}
main.cpp:
#include "Bookmark.h"
int main()
{
Bookmark resume{ 118, 7 };
resume.describe();
resume.advance();
resume.describe();
resume.advance(5);
resume.describe();
return 0;
}
resuming at page 118, line 7
resuming at page 119, line 1
resuming at page 124, line 1
Three details are worth pointing out.
Bookmark.cpp includes its own header first, then the system headers it needs. Including the class's own header at the top proves the header compiles on its own rather than by accident, and <iostream> has to be there because describe() uses std::cout. A header that forgets an include it depends on will compile in the one file that happens to include the right things first, and fail everywhere else.
main.cpp includes only the header. The compiler is satisfied by the declarations it finds there, and produces calls to functions it has never seen the bodies of. Those calls are resolved at link time, which means Bookmark.cpp must actually be compiled into the program. Adding the #include without adding the source file to the build produces an undefined reference at the link step, not a compiler error.
#pragma once guards the header against being included twice in the same translation unit, which happens easily once headers include other headers. A traditional pair of #ifndef and #define guards does the same job.
Put a class definition in a header file named after the class, and put its non-trivial member function definitions in a source file of the same name. Leave trivial members, such as accessors and constructors with empty bodies, defined inside the class.
Why Including A Class Definition Everywhere Is Legal
If ten source files include Bookmark.h, ten translation units end up containing a definition of Bookmark. That sounds like an obvious violation of the one definition rule, and it is not.
The ODR carves out an exception for types. Its one definition per program limit does not apply to them, so a class may be defined once in each translation unit, provided every one of those definitions is identical, which including the same header guarantees. Without that exemption, no class could ever be shared between files.
What the ODR still forbids is defining the same class twice inside a single translation unit, and that is exactly what #pragma once and header guards prevent.
Member functions get no such blanket exemption, which is where the inline distinction from the table comes in:
A member function whose body sits inside the class body is inline automatically, so it may appear in every translation unit that includes the header without breaking the ODR. A body written outside the class body carries no such marking, which is why it normally lives in a .cpp file where exactly one copy exists across the whole program.
Move an out-of-class definition into the header without marking it, and the program compiles but refuses to link as soon as two source files include that header. The linker reports a multiple definition of the function and names both object files it found it in.
When The Whole Class Belongs In The Header
The inline keyword lifts that restriction. An inline function may be defined in every translation unit, and the linker keeps one copy. So the out-of-class definitions can stay in the header if you say so explicitly:
Bookmark.h:
#pragma once
#include <iostream>
class Bookmark
{
private:
int m_page{};
int m_line{};
public:
Bookmark(int page, int line);
void describe() const;
void advance(int pages = 1);
int getPage() const { return m_page; }
int getLine() const { return m_line; }
};
inline Bookmark::Bookmark(int page, int line)
: m_page{ page }
, m_line{ line }
{
}
inline void Bookmark::describe() const
{
std::cout << "resuming at page " << m_page << ", line " << m_line << '\n';
}
inline void Bookmark::advance(int pages)
{
m_page += pages;
m_line = 1;
}
There is no Bookmark.cpp any more, and any number of files may include this header. Placing the definitions immediately below the class also has a second effect: inline expansion requires the compiler to have the full body in front of it, and a body sitting in a .cpp file is invisible to every other translation unit. A function defined just below the class in the same header is available to everyone who includes it, so the optimiser can expand the call if it decides that is worthwhile.
Four situations make the header-only arrangement the better choice:
- A class small enough to be used by exactly one .cpp file. Defining it there, entirely inside the class body, states plainly that it is local to that file. You can always split it out later.
- A class with one or two non-trivial members that are unlikely to change, where a source file holding two functions is more project clutter than it is worth.
- A library meant to be distributed header-only, so that using it requires an
#includeand nothing else. A source file has to be added to every project that consumes it, and that friction is what header-only distribution removes. - Class templates. A template member function defined outside the class is written in the header beneath the class, because the compiler needs the complete template definition in order to instantiate it. Class templates with member functions are covered in a later lesson.
The Price Of A Header Change
Header-only is not free, which is why it is not the default. Every translation unit that includes a header depends on its contents, so touching one line in Bookmark.h forces the build system to recompile every .cpp file that includes it, plus every file that includes a header that includes it. On a large project that ripple can turn a one-word edit into a full rebuild measured in hours.
Change a line in Bookmark.cpp instead and exactly one translation unit is recompiled, then relinked. The header, and therefore everybody's view of the class, has not moved.
That asymmetry is the practical argument for the split: the header holds the parts other files genuinely need to see, and the source file absorbs the changes nobody else has to know about.
Most examples in this course keep the class and all its member functions in a single file, because that is what fits in one runnable snippet. Real projects split classes into header and source files as a matter of routine, and it is worth building the habit early.
Default Arguments Go In The Class Definition
For a non-member function, a default argument goes on the forward declaration if there is one, and on the definition otherwise. A member function is always declared as part of the class definition, so the rule collapses to a single option: put the default in the class.
That is what void advance(int pages = 1); in the header does. The out-of-class definition writes void Bookmark::advance(int pages) with no default, because repeating it there is an error rather than a harmless duplicate. Every caller sees the header, so every caller sees the default.
Write a member function's default arguments in the class body, and nowhere else.
Where The Standard Library Keeps Its Implementations
You have been using classes split this way since your first program. #include <string> brings in declarations for std::string, yet you have never added a string.cpp to a project. The implementations were compiled long ago and sit in a library file that the linker pulls from automatically.
This is the same header-and-source split you have just applied to Bookmark, with the source half shipped as a precompiled binary instead of as text. Open source packages often hand you both the headers and the sources; commercial libraries usually ship headers plus a compiled library, for three reasons: linking an already-compiled library is faster than rebuilding it, one copy of it can be shared across many programs instead of being duplicated into each executable, and the source stays private.
Distributing your own library is beyond this course, but the prerequisite is the habit this lesson is about. A class whose declaration and implementation are already separated is one build step away from being distributable.
Summary
Separating declaration from implementation: Member function declarations must stay inside the class definition, but the bodies can be moved out. Prefix an out-of-class definition with the class name and the scope resolution operator, as in void Bookmark::describe() const, so the compiler knows it is a member of that class.
Header files for classes: Put the class definition in a header file named after the class so multiple source files can use the type. The compiler needs the complete class definition, not a forward declaration, because it must check member usage and calculate the size of an object of the type.
Code files for implementations: Put non-trivial member function definitions in a .cpp file carrying the class name. That file includes the class header first, then any system headers it needs, and it must be compiled into the program or the calls will fail to link.
Trivial functions stay in the class: One-line accessors and constructors with empty bodies are usually left inside the class definition, where they add almost no clutter.
Types are exempt from one definition per program: A class may be defined once per translation unit, so including its header everywhere is legal. Defining it twice in a single translation unit is not, which is what #pragma once and header guards prevent.
Implicit inline for bodies in the class body: A member function whose body sits inside the class body is inline automatically, and is therefore exempt from the one definition per program rule.
Explicit inline for bodies in a header: A body written outside the class body gets no automatic marking. Left unmarked in a header, it produces a multiple definition link error once two source files include that header, so it must be marked inline to stay there.
Inline expansion: The compiler can only expand a call it has the body for. Defining a member function as inline just below the class in the same header keeps it eligible for expansion in every file that includes the header.
Recompilation costs: Changing a header rebuilds every file that includes it, directly or indirectly. Changing a .cpp file rebuilds only that file, which is the main practical reason to keep non-trivial code out of headers.
Header-only classes: Small single-file classes, classes with a couple of stable non-trivial members, libraries meant for easy distribution, and class templates are all reasonable cases for keeping every definition in the header.
Default arguments location: Always put default arguments for member functions in the class definition, and never repeat them on the out-of-class definition.
Precompiled libraries: Standard library headers supply declarations while the implementations arrive as a precompiled binary at link time. Commercial libraries follow the same pattern for link speed, sharing, and source privacy.
Deciding where a definition lives is the same decision every time: put in the header what other files must see, and put in the source file everything else.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Organizing Class Definitions - Quiz
Test your understanding of the lesson.
Practice Exercises
Temperature Converter Class
Create a Temperature class with member functions defined both inside and outside the class definition. Practice separating declaration from implementation.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!