Lesson 02 / 14
Classes and Access Control
Measures which object a member function works with, and that access control operates at the level of the class, not the object.
Published Updated
In this lesson
- Confirms that a member function's this points at the object it was called on
- Applies the default access difference between class and struct, and public/private/protected
- Confirms by running it that access control operates at the level of the class, not the object
Contents
The previous lesson measured which object a free function works with by looking at its
parameter: shift(Point n) worked with its own copy, nudge(Point* p) reached the
caller’s object through an address. A member function also has a parameter, but the
compiler passes it implicitly: the address of the object the call was made on. This
implicit parameter’s name is this, and it is available throughout a member function’s
body — present even when not written. This lesson measures that same implicit parameter
with the same core instrument: when a member function is called, which object does
this point at, and how does that fact meet access control?
Classes and Access Control
class and struct are the same thing in C++; both define a type that can carry data
members and member functions, both can be inherited from, both run a constructor and a
destructor. The only difference between them is the default access level. A struct’s
members are public by default, a class’s members are private by default — no other
behavioral difference exists. This is an inheritance from C: in C, a struct was only a
data-carrying record, it carried no functions; when C++ gave struct the ability to
carry member functions, it kept the default access public for backward compatibility.
Using struct for types that mostly carry data with little behavior, and class for
types whose behavior is protected by access control, is a convention in a C++ codebase —
the language itself does not enforce it.
A class declares three access levels:
public— reachable from outside the class, from any code, anywhere. A class’s outward-facing interface lives here.private— reachable only from the class’s own member functions (and from functions or classes declared asfriend). The data members that hold a class’s own internal state are usually here.protected— reachable from the class’s own member functions and from derived classes; its difference fromprivateonly shows up under inheritance. Inheritance is not used in this course until M11/K04’s second topic, soprotectedis only named here.
An access-level declaration (public:, private:, or protected:) applies, inside a
class body, to every member after that point, up to the next access-level declaration.
In the Account class, the three member functions under public: are callable from
outside; balance_ under private: is not — the same class body can also contain more
than one public:/private: block, and the compiler merges them all.
A class carries data members (a variable with its own copy in every instance of the
object — balance_ carries its own value in every Account object) and member
functions (code shared across all instances of the class, reaching the called
instance’s data members through this — deposit’s machine code is generated once, not
separately for every Account object). If a member function is marked const, this
itself is const: the function cannot modify data members, only read them. balance()
and equals are const for this reason — both read balance_ without modifying it;
deposit is not const because it modifies balance_. Only const member functions
can be called on a const object; calling a non-const member function on a const
object is a compile error, because the compiler cannot guarantee that this will not be
modified inside the function.
A friend declaration grants a function or class, which is not itself a member, access
to a class’s private members — the sole exception to access control, and one granted
explicitly by the class’s own declaration, never taken from outside. In this course,
friend is used exactly once, so that operator<< can read a class’s private data
member (in the Operator Overloading lesson); here it is only named.
Encapsulation — hiding a class’s internal state from the outside and exposing access
only through functions — was already covered in M08; it is not retaught here, only
applied through C++’s public/private syntax. Account applies this idea: balance_
cannot be written to directly from outside, it can only be changed through deposit.
A Member Function’s this
this is an implicitly defined pointer inside a member function’s body; its type is
determined by the class it was called on. Inside Account::deposit, this’s type is
Account* const — the pointer itself cannot be changed (it cannot start pointing at a
different Account), but the object it points at can be modified. Inside a const
member function — such as Account::balance() const — this’s type is const Account* const: neither the pointer nor the object it points at can be modified, which is why a
const member function only gains read access to data members. Code does not write this
pointer by hand — balance_ += amount is read by the compiler as this->balance_ += amount — but the measurement below shows plainly that this really does point at the
called object, not some other object: the address &h captured before the call to
h.deposit(50) is compared against the equivalent of this used implicitly inside the
function.
// account.hpp — an account whose balance is private
#ifndef ACCOUNT_HPP
#define ACCOUNT_HPP
class Account {
public:
void deposit(int amount) { balance_ += amount; }
int balance() const { return balance_; }
bool equals(const Account& other) const { return balance_ == other.balance_; }
private:
int balance_ = 0;
};
#endif
// this-measurement.cpp — does the member function's this point at the called object
#include <cstdio>
#include "account.hpp"
int main() {
Account a;
Account* before = &a;
a.deposit(50);
std::printf("does this point at the called object %s\n", before == &a ? "yes" : "no");
std::printf("balance %d\n", a.balance());
return 0;
}
does this point at the called object yes balance 50
CR3. The compiler passes &a as this in the call a.deposit(50); the line
balance_ += amount inside deposit modifies a.balance_, no other Account. This
expression is of kind self: it works with a itself.
Access Control Asks About the Class, Not the Object
balance_ is private; it cannot be written from outside Account — a line like that
attempted inside main is rejected by the compiler, because main is not a member of
the Account class. But Account‘s own member function can reach directly into
another Account object’s balance_ — access control asks about the class, not the
object. equals shows this: other.balance_ is not the caller’s own balance_, but it
compiles because it belongs to the same class. This is a C++ rule that does not hold in
many languages — in some languages access control works at the object level and an
object can only touch its own data; in C++ the boundary is the class itself, not a
single object.
// equals-measurement.cpp — a member function reaching another object's private member
#include <cstdio>
#include "account.hpp"
int main() {
Account a;
Account b;
a.deposit(30);
b.deposit(30);
std::printf("are a and b equal %s\n", a.equals(b) ? "yes" : "no");
b.deposit(1);
std::printf("are a and b equal %s\n", a.equals(b) ? "yes" : "no");
return 0;
}
are a and b equal yes are a and b equal no
CR4. The access other.balance_ inside a.equals(b) reaches b’s private member
from a’s member function; the compiler accepts this only by looking at other’s type
(const Account&), not at whether a and b happen to be the same object. The access
decision is made at compile time, from the class information.
This is one of the points where many C++ learners are first surprised: private is
assumed to mean “only this object can access its own data,” but the actual rule is
“only this class’s member functions can access it.” While equals’s body is
compiled, the compiler only knows this: other is a const Account&, and the function
currently being compiled is a member of the Account class; these two facts are enough
to permit the access other.balance_. Whether a and other (that is, b) are the
same runtime object is not known at compile time and plays no part in the access
decision at all — indeed, in the second call a and b are separate objects and the
access still compiles.
Why private Protects the Class, Not the Object
This rule is not arbitrary; it makes it possible for a class’s author to protect its
invariants (a condition that must always hold for an object — for example, that
balance_ must never drop below zero). If private only meant “reachable from your own
object, not from other objects,” functions like equals that compare two objects, or
functions that copy data from one object into another, could not be written — every
member function could only look at its own this. Class-level access lets a class’s own
member functions safely share data among themselves, while closing that same data off to
code outside the class. The boundary it draws is not the object — it is the
declaration in the class’s own source file.
The opposite of this is a struct with an open field: in a record like that, calling code
can change that field without passing through any check at all. Account does not make
that mistake — there is no path to write balance_ from outside, it can only be changed
through deposit. deposit today does nothing beyond balance_ += amount, but because
of private, a rule could later be added — rejecting a negative amount, or enforcing
an upper bound — and that rule could be written into deposit alone, without changing
any caller, because the only path to balance_ is deposit. This is the real
engineering value private buys: the freedom to change the class’s internal
representation without touching the code outside it.
Choosing Which Call a const Object Allows
The question of why balance() and equals are marked const while deposit is not
has a practical consequence: a function taking a const Account& parameter can only
call const member functions through that parameter. equals‘s signature is bool equals(const Account& other) const; its body accesses other.balance_ directly, but it
would also compile if it called other.balance() instead — because balance() is
const and a const function can be called through a const reference. Had
other.deposit(10) been attempted, it would not compile: deposit is not const, and
a non-const member function cannot be called on a const object (or on the object a
const reference points at). This rule shows that writing const in a function’s
signature is not merely a documentary note — it is a contract the compiler actually
enforces: that equals will not modify other is verified at compile time from the
function’s own signature.
The same rule applies to this. Because Hesap::equals — Account::equals — is marked
const, its own this is also const Account* const; if a change such as
balance_ += 1 were attempted inside this function’s body — even on its own object —
the compiler rejects it. A const member function can modify neither its own object nor
another object it received by const reference; the only things it can modify are local
variables and data members marked mutable (mutable is a marker reserved for rare
cases such as caching, and it is not used in this course).
Declaration Order and Access-Level Blocks
In the Account class, the access-level declarations follow a particular layout order:
first the member functions exposed to the outside under public:, then the data member
under private:. This order is not a rule of the language — the compiler would produce
the same result if private: came first and public: came after — but it is a common
habit for readability: someone reading a class first sees the interface it offers, then
the internal representation. The reverse order (private data first, public functions
after) is equally valid and preferred in some codebases; this lesson follows the
public-first order and stays consistent with it for the rest of the course.
An access-level declaration is not required to be used only once — public: and
private: can each repeat more than once within the same class body, each covering the
block that follows it. Account does not do this; because the class is small, a single
public: and a single private: are enough. In a larger class, using more than one
block to keep related members together — a separate public: for constructors, a
separate public: for query functions, for example — can improve readability; this is a
style choice, it does not change the compiler’s behavior.
The Bounding Measurement
The expression this lesson measures is of kind self: h.deposit(50) writes to h
itself. But the boundary of access control is visible here too: “who accesses” a
private member is asked at the level of the class, not the object. The call to
other.balance_ inside equals proves this — a private member of a different object
of the same class can be reached, without that object’s own function, from just another
member function of the same class. private protects the class’s boundary, not the
object.
This boundary has a further consequence: access to a private member is possible from
every member function in the class’s own source code — the number of such member
functions can grow as the class grows, and each one carries the same risk of breaking
the same invariant. Account is small, with three member functions, and all of them are
visible in this lesson; a large class can have dozens of member functions that reach
private data, and the rule measured in this lesson — that access is tied to the class,
not the object — remains unchanged at that scale too. private answers the question “how
many places can reach this” not with a count of objects, but with the class’s count of
member functions.
These two points — which object a given expression works with, and that access control
asks about the class, not the object — together explain why a class can draw a reliable
boundary. this points at the correct object on every call, so a member function never
confuses its own object with another; private grants every member function access to
every object of the same class, so the class can write operations that require sharing
data internally (comparison, copying, merging). These two rules together lay the ground
for the next lesson’s subject, the constructor: a constructor is also a member function,
it has its own this, and it reaches private data members with the same freedom — but
what sets it apart is that the object it is called on does not yet exist.
Summary
- The expression this lesson measures is of kind self: in the call
h.deposit(50),thisequals&h. - The only difference between
classandstructis the default access level;public/private/protecteddeclare three access levels. - Inside a
constmember function,thisis itselfconst; data members cannot be modified. - Access control operates at compile time and at the level of the class: a
member function can reach a
privatemember of a different object of the same class. - Encapsulation was defined in M08; here it was only applied through
class/privatesyntax.
Next Step
This lesson showed which object a member function works with; the next lesson asks the
same question of a constructor — it measures that an argument implicitly built from
an int is an unnamed, temporary object, and exactly when that object ceases to exist.
To keep your progress and take notes, Log in
My notes
Log in to take notes.