C++ Part I · Module 11 · Classes and Encapsulation
Data Members and Member Functions
The classes written so far have used member functions without explaining them, including one that carried a const nobody accounted for. This chapter is about them properly, and it has an easy half and a hard half. The easy half is the syntax, with one genuine trap in it: a member function may be defined inside the class body or outside it, both are legal and most introductions present the choice as a matter of taste, and it is not. The difference is whether the definition is implicitly inline, which is invisible in a single file and becomes a duplicate-symbol error from the linker the moment two source files include the same header — measured here, because it is the kind of thing that is far easier to recognise once than to debug later. The chapter also settles how a member function reaches its object's data when nothing was passed to it, and offers an unexpectedly blunt piece of evidence that the code is not stored in the object: for a class holding a single int, the object measured four bytes and a pointer to one of its member functions measured sixteen. The hard half has no syntax at all. Given an operation that involves a type, deciding whether it belongs inside that type is the actual design work, it is the thing that separates a well-shaped class from a bag of methods, and the answer follows from the previous chapters rather than from taste.
Foundation17 min readC++Part IFundamentalsClasses
Part I · Module 11 · Chapter 11.3 · Data Members and Member Functions
1. State and Behaviour in One Place
A class body holds two kinds of thing:
Data members — the state. Each object gets its own copy, as Chapter 11.2 §7 measured.
Member functions — the behaviour. One copy, shared by all objects, as Chapter 11.1 §4 measured when ten of them added zero bytes.
class Samples {
public:
int count() const { return n_; } // behaviour
void add(int v);
private:
int n_ = 0; // state
double sum_ = 0.0;
};The reason to write them together is the previous two chapters' argument: the functions are the only code permitted to touch the data, so they are the only place the invariant can be maintained.
2. Data Members
int n_ = 0; // with a default member initializer
double sum_ = 0.0;Everything from Chapter 9.8 applies: members are laid out in declaration order, padding is inserted for alignment, and sizeof is not the sum of the members. Measured for Samples:
sizeof(Samples) = 16 (int + double, 3 member functions)An int and a double total 12 bytes and the type is 16 — Chapter 9.8's padding rule, and Chapter 9.8 §8's advice to declare the widest members first applies to classes exactly as it did to structs. The three member functions contribute nothing.
Give every data member a default member initializer unless a constructor will certainly set it. Chapter 11.2 §8's callout made the case: it costs nothing and it removes Chapter 3.8's indeterminate-value category, which Chapter 10.3 §5 measured drawing zero warnings for a dynamic object.
3. Member Functions: Two Places to Define Them
class Samples {
public:
Samples() = default;
// defined INSIDE the class body
int count() const { return n_; }
// declared here, defined OUTSIDE the class
void add(int v);
double mean() const;
private:
int n_ = 0;
double sum_ = 0.0;
};
// out-of-class definitions need the Samples:: qualification
void Samples::add(int v) { ++n_; sum_ += v; }
double Samples::mean() const { return n_ ? sum_ / n_ : 0.0; }Measured:
count=3 mean=20.00
sizeof(Samples) = 16
a.count()=1 b.count()=0Three mechanical points:
Out-of-class definitions need Samples:: — without it you have defined an unrelated free function.
The const must be repeated on the out-of-class definition. It is part of the function's signature, not a decoration; Chapter 11.7 is the subject, and this is the second chapter to use it unexplained — deliberately, because explaining it needs Chapter 11.6's this first.
A declaration in the body plus a definition outside is the normal arrangement for anything non-trivial, with the class in a header and the definitions in a .cpp.
4. The Difference Is Not Style — Measured
Most introductions present the two placements as taste. They are not equivalent, and the difference is invisible until a second source file appears.
A header defining one member function each way, with no inline on the out-of-line one:
#ifndef WIDGET_H
#define WIDGET_H
class Widget {
public:
int inBody() const { return 1; } // defined in the body
int outOfLine() const; // declared here
};
int Widget::outOfLine() const { return 2; } // defined out of line, in the header
#endifTwo source files include it. Each compiles alone with 0 errors. Then:
duplicate symbol 'Widget::outOfLine() const' in:
ld: 1 duplicate symbols
clang++: error: linker command failed with exit code 1Adding inline to the out-of-line definition: links, runs, prints 3 3.
And the decisive half of the same experiment: inBody() was compiled into both object files and produced no duplicate-symbol error at all.
A member function defined inside the class body is implicitly
inline. One defined outside it is not.
Three consequences worth keeping:
It is a linker error, not a compiler error. Each translation unit was individually fine. Chapter 2.6 established the distinction and Chapter 2.7 is how to read such messages; this is the same shape as Chapter 11.8's missing static data member definition, which also compiles cleanly and fails to link.
So out-of-line definitions belong in a .cpp file, where they are compiled once. That is the default arrangement and it needs no inline.
And if an out-of-line definition must be in a header — a small accessor you want inlined, or a header-only library — it needs inline explicitly. Chapter 6.8 measured what inline does and does not mean: it is a statement about linkage and the One-Definition Rule, not a demand to the optimiser.
5. How a Member Function Reaches the Data
count() returns n_ and nothing was passed to it. Two objects call the same function and get different answers — Chapter 11.2 §7 measured 3, 1 and 0.
The mechanism, in words: a member function receives the object it was called on as a hidden argument. a.count() passes a; b.count() passes b; inside the function, an unqualified n_ means "the n_ of whichever object this call was given".
So the two lines below do the same work with different syntax:
a.count(); // member function: the object is implicit
count_of(a); // free function: the object is a parameterThe hidden argument has a name, this, and it is Chapter 11.6's subject — including the measurement that it is not stored in the object.
6. The Code Is Not in the Object
Chapter 11.1 §4 showed ten member functions adding 0 bytes. Here is the blunter version:
class C {
public:
int a() const { return 1; }
int b() const { return 2; }
int n_ = 0;
};
auto pa = &C::a; // a pointer to a member functionsizeof(C) = 4 sizeof(&C::a) = 16
(x.*pa)() = 1 (y.*pa)() = 1 same function, two objectsThe object is 4 bytes. A pointer to one of its member functions is 16 — four times the size of the object it belongs to. If the functions lived in the object, the object could not possibly be smaller than a pointer to one of them.
(Why 16 rather than 8 is a detail of how such pointers must work in general, which belongs with Module 17's virtual functions. The fact that matters here is the inequality.)
The (x.*pa)() and (y.*pa)() calls are the same function applied to two objects, which is §5's mechanism made explicit — the object is supplied at the call, not baked into the function.
7. Which Operations Belong Inside?
No new syntax from here, and this is the part that is actually difficult.
The temptation is to put every operation that mentions the type inside the type, which produces a class with forty members and no shape. The question has a principled answer, and it comes from Chapter 11.1:
An operation belongs inside the type if it needs access to the private data, or if it must maintain the invariant.
Four tests, in order of how often they settle it.
Does it need the private data? mean() needs n_ and sum_. It has to be a member, or it has to be given access deliberately.
Does it maintain the invariant? Anything that mutates the object must be a member, because a non-member cannot be trusted with it. Chapter 11.1 §5's widen() is the example: it is the only legitimate way to change the width because it is the only code that re-checks the rule.
Can it be written using only the public interface? Then it probably should be a free function. A function computing the midpoint of a Field from offset() and width() needs nothing private, so making it a member adds to the class's surface for no benefit.
Is it fundamental to what the type is? add and mean are what Samples is for. A function formatting a Samples as a CSV line is about output, not about sampling.
8. Why a Smaller Interface Is Better
The three tests above all push work out of the class, and that is the right direction, for reasons this curriculum has already measured.
Every public member is a promise. Chapter 6.9 §2 argued that a signature is a contract; a class's public interface is a contract with as many clauses as it has members, and all of them have to keep working.
Every member can see the private data, so every member is somewhere the invariant can be broken. A class with four members has four places to check when the invariant is violated; one with forty has forty. Chapter 11.5 makes this precise.
A free function cannot break the invariant, because it has no access. That is a guarantee, not a hope.
And fewer members means a smaller thing to understand. Chapter 6.9 §3's one-job-each argument applies to types as well as functions.
Two mechanisms for operations that stay outside: a plain free function in the same namespace (Chapter 7.6), which is the usual answer; and for operators specifically, Chapter 15.2's member-versus-non-member question, which has its own rules.
There is also a facility for granting a specific outside function access to private data — friend — which exists and which is the correct answer surprisingly rarely. It is worth knowing the name so you recognise it; reaching for it usually means the interface is wrong.
9. What to Actually Do
Declare data members private and give them default member initializers.
Declare the widest members first, per Chapter 9.8 §8 — padding applies to classes identically.
Define trivial accessors in the class body; they are implicitly inline and that is what you want.
Put non-trivial definitions out of line, in a .cpp — no inline needed there.
If an out-of-line definition must live in a header, mark it inline. Measured: otherwise it is duplicate symbol at link.
Repeat the const on out-of-class definitions; it is part of the signature.
Make it a member if it needs private data or maintains the invariant — otherwise prefer a free function.
Prefer immutability, then named operations; avoid a setter per member.
Keep the public interface small. Every member is a promise and a place the invariant can break.
10. Mistakes Worth Making Once
Omitting Samples:: on an out-of-class definition — you have written an unrelated free function.
Defining a member function out of line in a header without inline. Measured: compiles per file, duplicate symbol at link.
Assuming the two placements are a style choice. One is implicitly inline; the other is not.
Forgetting the const on the out-of-class definition. It does not match the declaration.
Writing a getter and setter for every member. Chapter 11.1 §2's struct, with more typing.
Putting every operation that mentions the type inside the type. Forty members, no shape, forty places to break the invariant.
Expecting member functions to cost object size. Measured: 0 bytes, and a pointer to one is bigger than the object.
Ignoring member order. int then double gave 16 bytes for 12 bytes of data.
Reaching for friend to fix an awkward interface. Usually the interface is the problem.
11. Interview Relevance
"Where can you define a member function, and does it matter?" Inside the body or out of line — and it matters: in-body is implicitly inline, so an out-of-line definition placed in a header without inline is a duplicate symbol at link time. Being able to name the failure as a linker error is the distinguishing detail.
"How does a member function know which object it was called on?" It receives it as a hidden argument — this. Good follow-up: it is not stored in the object, which the sizes prove.
"Are member functions stored in each object?" No. Measured: ten added 0 bytes, and a pointer to a member function was 16 bytes against a 4-byte object.
"How do you decide whether something should be a member?" If it needs private access or maintains the invariant, it is a member; if it can be written against the public interface, prefer a free function. This is a design question and vague answers are common.
"What is wrong with a getter and setter for every field?" It reproduces exactly what a struct permits while looking like encapsulation. The strong answer offers the alternatives: immutability, or named operations that validate.
12. What You Should Be Able to Say Now
- A class holds data members (one copy per object) and member functions (one copy, shared).
- Measured:
sizeof(Samples)is 16 for anintand adouble— Chapter 9.8's padding, unchanged for classes. - Out-of-class definitions need the
Class::qualification and must repeat theconst. - A member function defined in the class body is implicitly
inline; one defined outside is not. - Measured: an out-of-line definition in a header gave
duplicate symbol 'Widget::outOfLine() const'andld: 1 duplicate symbols— after each file compiled with 0 errors. - Adding
inlinefixed it, and the in-body function was in both translation units with no duplicate-symbol error — proving it is implicitlyinline. - It is a linker error, not a compiler error (Chapters 2.6 and 2.7).
- A member function receives its object as a hidden argument — Chapter 11.6's
this. - Measured:
sizeof(C)was 4 andsizeof(&C::a)was 16 — the code is not in the object. - An operation belongs inside the type if it needs private data or maintains the invariant; otherwise prefer a free function.
- A smaller public interface is better — every member is a promise and a place the invariant can break.
- A getter and setter per member reproduces a
struct— prefer immutability, then named validating operations. - If nothing must stay true, the right answer is a
struct, not accessors.
13. Exercises
Exercise 1 — place them. For a Samples class, say where you would define count(), add() and mean(), and why.
Exercise 2 — predict the failure. A header contains double Samples::mean() const { ... } with no inline, included by three .cpp files. Say what happens, at which stage, and give two fixes.
Exercise 3 — prove it. Describe the two-file experiment that shows an in-body definition is implicitly inline, and say what result proves it.
Exercise 4 — member or free? Decide for each on a Field with private offset_/width_: (a) last() (b) mask() (c) widen() (d) toCsvLine() (e) overlaps(const Field&) (f) isByteAligned().
Exercise 5 — the sizes. Explain what sizeof(C) = 4 and sizeof(&C::a) = 16 together prove, and why the second is not 8.
Exercise 6 — fix the interface. Rewrite the getter/setter Field from the callout so it has a real invariant. State the invariant and list the members you kept.
Exercise 7 — order the members. Given char flag; double value; int count;, give the declaration order you would choose and the sizeof you expect, citing Chapter 9.8.
14. Summary
- A class holds data members (per object) and member functions (shared, 0 bytes).
- Measured:
sizeof(Samples)16 for 12 bytes of members — Chapter 9.8's padding applies unchanged. - Out-of-class definitions need
Class::and must repeat theconst. - In-body definitions are implicitly
inline; out-of-line ones are not — and this is not a style choice. - Measured:
duplicate symbol 'Widget::outOfLine() const'at link, after both files compiled cleanly;inlinefixed it; the in-body function never collided. - A member function gets its object as a hidden argument (Chapter 11.6).
- Measured: object 4 bytes, pointer-to-member-function 16 — the code is not in the object.
- Inside if it needs private data or maintains the invariant; otherwise a free function.
- Keep the interface small — every member is a promise and a place to break the invariant.
- A getter and setter per member is a
structwith more typing — prefer immutability or named operations.
The mechanism is in place and one word has now been deferred three times. Every listing in this module has used public: and private: without saying what they mean beyond "reachable" and "not", and the whole argument of Chapter 11.1 rests on them: the invariant is enforceable only because the data cannot be reached from outside, which is a claim about those two labels and nothing else. The next chapter is the mechanism itself — the three specifiers, the one measured difference between class and struct that is the only technical distinction between the two keywords, and a question with a slightly uncomfortable answer about whether mixing access sections changes where your members actually sit in memory. The compiler's own warning on that last point turned out to be more informative than the measurement.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
