Skip to content
academia.sh

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 as friend). 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 from private only shows up under inheritance. Inheritance is not used in this course until M11/K04’s second topic, so protected is 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), this equals &h.
  • The only difference between class and struct is the default access level; public/private/protected declare three access levels.
  • Inside a const member function, this is itself const; data members cannot be modified.
  • Access control operates at compile time and at the level of the class: a member function can reach a private member of a different object of the same class.
  • Encapsulation was defined in M08; here it was only applied through class/private syntax.

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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close