Organizing Class Definitions
Split class definitions between header and source files for better compilation.
What Are Classes and Header Files?
As classes grow more complex, it becomes practical to separate their declarations into header files and their member function definitions into source files. This lesson covers how to organize class code across multiple files.
So far, we've written classes simple enough to implement member functions directly inside the class definition. For example, here's a basic Rectangle class with all member functions defined inside:
#include <iostream>
class Rectangle
{
private:
double m_width{};
double m_height{};
public:
Rectangle(double width, double height)
: m_width{ width }
, m_height{ height }
{
}
void print() const { std::cout << "Rectangle(" << m_width << " x " << m_height << ")\n"; }
double getWidth() const { return m_width; }
double getHeight() const { return m_height; }
double getArea() const { return m_width * m_height; }
};
int main()
{
Rectangle rect{ 5.0, 3.0 };
rect.print();
return 0;
}
However, as classes become larger and more complex, having all member function definitions inside the class makes it harder to manage and work with. Using a well-designed class only requires understanding its public interface (the public member functions), not the implementation details. Member function implementations clutter the public interface with details irrelevant to using the class.
To address this, C++ allows us to separate the class's "declaration" from its "implementation" by defining member functions outside the class definition.
Here's the same Rectangle class with the constructor and print() member function defined outside. The prototypes remain inside the class definition (they must be declared as part of the class), but the actual implementations are moved outside:
#include <iostream>
class Rectangle
{
private:
double m_width{};
double m_height{};
public:
Rectangle(double width, double height); // constructor declaration
void print() const; // print function declaration
double getWidth() const { return m_width; }
double getHeight() const { return m_height; }
double getArea() const { return m_width * m_height; }
};
Rectangle::Rectangle(double width, double height) // constructor definition
: m_width{ width }
, m_height{ height }
{
}
void Rectangle::print() const // print function definition
{
std::cout << "Rectangle(" << m_width << " x " << m_height << ")\n";
};
int main()
{
const Rectangle rect{ 5.0, 3.0 };
rect.print();
return 0;
}
Member functions can be defined outside the class definition just like non-member functions. The only difference is we must prefix the member function names with the class name (in this case, Rectangle::) so the compiler knows we're defining a member of that class rather than a non-member function.
Note that we left the accessor functions defined inside the class definition. Because accessor functions are typically one line, defining them inside adds minimal clutter, whereas moving them outside would create many extra lines of code. For this reason, trivial one-line functions are often left inside the class definition.
Putting class definitions in a header file
If you define a class inside a source (.cpp) file, that class is only usable within that particular file. In larger programs, we often want to use classes in multiple source files.
In lesson 2.11 -- Header files, you learned that function declarations can be placed in header files, then #included into multiple code files. Classes work the same way. A class definition can be put in a header file, then #included into any other file that needs to use it.
Unlike functions (which only need a forward declaration to be used), the compiler typically needs to see the full class definition to use the type. This is because the compiler needs to understand how members are declared to ensure proper use, and it needs to calculate how large objects of that type are to instantiate them. So header files usually contain the full class definition rather than just a forward declaration.
Naming your class header and code files
Most often, classes are defined in header files with the same name as the class, and any member functions defined outside the class are put in a .cpp file with the same name.
Here's our Rectangle class split into .cpp and .h files:
Rectangle.h:
#ifndef RECTANGLE_H
#define RECTANGLE_H
class Rectangle
{
private:
double m_width{};
double m_height{};
public:
Rectangle(double width, double height);
void print() const;
double getWidth() const { return m_width; }
double getHeight() const { return m_height; }
double getArea() const { return m_width * m_height; }
};
#endif
Rectangle.cpp:
#include "Rectangle.h"
#include <iostream>
Rectangle::Rectangle(double width, double height) // constructor definition
: m_width{ width }
, m_height{ height }
{
}
void Rectangle::print() const // print function definition
{
std::cout << "Rectangle(" << m_width << " x " << m_height << ")\n";
}
Now any other header or code file that wants to use the Rectangle class can simply #include "Rectangle.h". Note that Rectangle.cpp must also be compiled into any project using Rectangle.h so the linker can connect function calls to their definitions.
Prefer to put your class definitions in a header file with the same name as the class. Trivial member functions (such as accessor functions, constructors with empty bodies, etc.) can be defined inside the class definition.
Prefer to define non-trivial member functions in a source file with the same name as the class.
Doesn't defining a class in a header file violate the one-definition rule if the header is #included more than once?
Types are exempt from the part of the one-definition rule (ODR) that says you can only have one definition per program. Therefore, there isn't an issue #including class definitions into multiple translation units. If there was, classes wouldn't be of much use.
Including a class definition more than once into a single translation unit is still an ODR violation. However, header guards (or #pragma once) prevent this from happening.
Inline member functions
Member functions are not exempt from the ODR, so you may wonder how we avoid ODR violations when member functions are defined in a header file that's included in multiple translation units.
Member functions defined inside the class definition are implicitly inline. Inline functions are exempt from the one definition per program part of the one-definition rule.
Member functions defined outside the class definition are not implicitly inline (and thus are subject to the one definition per program part of the ODR). This is why such functions are usually defined in a code file (where they'll only have one definition across the program).
Alternatively, member functions defined outside the class definition can be left in the header file if made inline using the inline keyword. Here's our Rectangle.h header with member functions defined outside the class marked as inline:
Rectangle.h:
#ifndef RECTANGLE_H
#define RECTANGLE_H
#include <iostream>
class Rectangle
{
private:
double m_width{};
double m_height{};
public:
Rectangle(double width, double height);
void print() const;
double getWidth() const { return m_width; }
double getHeight() const { return m_height; }
double getArea() const { return m_width * m_height; }
};
inline Rectangle::Rectangle(double width, double height) // now inline
: m_width{ width }
, m_height{ height }
{
}
inline void Rectangle::print() const // now inline
{
std::cout << "Rectangle(" << m_width << " x " << m_height << ")\n";
}
#endif
This Rectangle.h can be included into multiple translation units without issue.
Core Understanding
Functions defined inside the class definition are implicitly inline, which allows them to be #included into multiple code files without violating the ODR.
Functions defined outside the class definition are not implicitly inline. They can be made inline by using the inline keyword.
Inline expansion of member functions
The compiler must see a full function definition to perform inline expansion. Most often, such functions (e.g., accessor functions) are defined inside the class definition. However, if you want to define a member function outside the class definition but still want it eligible for inline expansion, you can define it as an inline function just below the class definition (in the same header file). That way the definition is accessible to anyone who #includes the header.
So why not put everything in a header file?
You might be tempted to put all member function definitions in the header file, either inside the class definition or as inline functions below it. While this compiles, there are downsides.
First, as mentioned, defining members inside the class definition clutters it up.
Second, if you change code in the header, every file that includes that header must be recompiled. This can have a ripple effect, where one minor change causes the entire program to need recompilation. Recompilation cost varies significantly: a small project may take only a minute, whereas a large commercial project can take hours.
Conversely, if you change code in a .cpp file, only that .cpp file needs recompilation. Therefore, given the choice, it's generally better to put non-trivial code in a .cpp file when possible.
There are cases where it makes sense to violate the best practice of putting the class definition in a header and non-trivial member functions in a code file.
First, for a small class used in only one code file and not intended for general reuse, you may prefer to define the class (and all member functions) directly in the single .cpp file using it. This makes it clear the class is only used within that file and isn't intended for wider use. You can always move the class to separate header/code files later if needed.
Second, if a class has only a few non-trivial member functions unlikely to change, creating a .cpp file containing only one or two definitions may not be worth the effort (as it clutters your project). In such cases, making the member functions inline and placing them beneath the class definition in the header may be preferable.
Third, in modern C++, classes or libraries are increasingly distributed as "header-only", meaning all code is in a header file. This is done primarily to make distribution and use easier, as a header only needs to be #included, whereas a code file needs to be explicitly added to every project using it for compilation. If intentionally creating a header-only class or library for distribution, all non-trivial member functions can be made inline and placed in the header beneath the class definition.
Finally, for template classes, template member functions defined outside the class are almost always defined in the header file beneath the class definition. Just like non-member template functions, the compiler needs to see the full template definition to instantiate it. We cover template member functions in lesson 15.5 -- Class templates with member functions.
In future lessons, most classes will be defined in a single .cpp file with all functions implemented directly in the class definition. This is done to keep examples concise and easy to compile. In real projects, it's much more common for classes to be in their own code and header files, and you should get used to doing so.
Default arguments for member functions
In lesson 11.5 -- Default arguments, we discussed the best practice for default arguments of non-member functions: "If the function has a forward declaration (especially one in a header file), put the default argument there. Otherwise, put the default argument in the function definition."
Because member functions are always declared (or defined) as part of the class definition, the best practice for member functions is simpler: always put the default argument inside the class definition.
Put any default arguments for member functions inside the class definition.
Libraries
Throughout your programs, you've used classes from the standard library, such as std::string. To use these classes, you simply #include the relevant header (such as #include <string>). Note that you haven't needed to add any code files (such as string.cpp or iostream.cpp) to your projects.
The header files provide the declarations the compiler requires to validate that your programs are syntactically correct. However, the implementations for classes belonging to the C++ standard library are contained in a precompiled file linked in automatically at the link stage. You never see the code.
Many open source software packages provide both .h and .cpp files for you to compile into your program. However, most commercial libraries provide only .h files and a precompiled library file. There are several reasons for this: 1) It's faster to link a precompiled library than to recompile it every time you need it, 2) A single copy of a precompiled library can be shared by many applications, whereas compiled code gets compiled into every executable that uses it (inflating file sizes), and 3) Intellectual property reasons (you don't want people stealing your code).
We discuss how to include 3rd party precompiled libraries in your projects in the appendix.
While you probably won't be creating and distributing your own libraries for a while, separating your classes into header and source files is not only good form, it also makes creating your own custom libraries easier. Creating your own libraries is beyond the scope of these tutorials, but separating declaration and implementation is a prerequisite to doing so if you want to distribute precompiled binaries.
Summary
Separating declaration from implementation: Move member function implementations outside the class definition to declutter the interface. Keep prototypes inside the class, but define implementations outside using the ClassName:: scope qualifier.
Header files for classes: Put class definitions in header files with the same name as the class. This allows the class to be included in multiple source files. The compiler needs the full class definition to use the type, not just a forward declaration.
Code files for implementations: Put non-trivial member function definitions in a .cpp file with the same name as the class. This file must #include the class header and be compiled into your project.
Trivial functions stay in the class: One-line functions like accessors and constructors with empty bodies can remain defined inside the class definition for convenience, as they add minimal clutter.
Implicit inline for class-defined functions: Member functions defined inside the class definition are implicitly inline, exempting them from ODR violations when the header is included in multiple translation units.
Explicit inline for header-defined functions: Member functions defined outside the class in a header file must be marked inline to avoid ODR violations. This allows keeping all code in the header when desired.
Recompilation costs: Changes to header files require recompiling all files that include them. Changes to .cpp files only require recompiling that one file. For large projects, prefer .cpp files for implementations to minimize recompilation time.
Header-only classes: Small classes, classes with few non-trivial functions, or classes intended for wide distribution are sometimes kept entirely in headers using inline for external definitions.
Default arguments location: Always put default arguments in the class definition within the header file, not in the external function definition.
Precompiled libraries: Commercial libraries typically provide headers and precompiled library files rather than source code for speed, file size, and intellectual property protection.
Proper organization of class code across headers and source files is essential for maintainability, compilation efficiency, and professional C++ development.
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!