Access Specifiers
One of the central ideas in object-oriented programming is that an object's internal state — its data — should be controlled by the object itself, not manipulated freely from outside. C++ enforces this through access specifiers: labels in the class definition that determine which members of a class are visible to code outside it. Three specifiers exist: public (accessible everywhere), private (accessible only within the class), and protected (accessible within the class and in derived classes). Getting access control right is the foundation of reliable, maintainable class design.
public and private: the core distinction
Members declared under a public:label are part of the class's interface — the operations the outside world is allowed to perform on an object. Members declared under private: are the class's implementation — internal details that callers should never need to touch. By hiding implementation details behind private access, you can change how the class works internally without breaking any code that uses it. This separation of interface from implementation is called encapsulation, and it is one of the most practical benefits of OOP.
Here is the same Box class written both ways to make the contrast vivid. In the first version, all members are public — anyone can set a dimension to a negative number and the class cannot stop them. In the second version, the data is private and the class controls all access to it:
// Dangerous: no protection — anyone can write box.m_length = -5.0;
struct Box {
double m_length {1.0};
double m_width {1.0};
double m_height {1.0};
double volume() { return m_length * m_width * m_height; }
};
// Safe: private data, public interface
class Box {
public:
double volume() { return m_length * m_width * m_height; }
private:
double m_length {1.0};
double m_width {1.0};
double m_height {1.0};
};Notice the two keywords: struct and class. In C++ they are nearly identical — the only difference is the default access. A struct makes all members public unless you say otherwise; a class makes all members private unless you say otherwise. By convention, struct is used for simple data bundles and class for types with meaningful encapsulation.
Naming conventions: m_ prefix and public-first ordering
C++ programmers have two near-universal conventions for class definitions. First, private member variables are usually prefixed with m_ (for “member”) — so m_length instead of just length. This makes it immediately obvious inside a member function body which names are instance variables and which are local variables or parameters. Second, public members are declared before private ones. This ordering puts the interface — what callers care about — at the top, where it is easy to find, and pushes the implementation details to the bottom.
class Box {
public: // interface at the top
Box() = default;
Box(double length, double width, double height);
double volume() const;
private: // implementation below
double m_length {1.0}; // m_ prefix signals member variable
double m_width {1.0};
double m_height {1.0};
};Getters and setters: controlled access to private data
Making data private does not mean callers can never read it — it means callers can only read it through a controlled channel. Functions that return the value of a private member variable are called getters (or accessors). Functions that change a private member variable are called setters (or mutators). The advantage over making the variable public is that a setter can validate or sanitize the new value before storing it — for example, refusing a negative dimension:
class Box {
public:
Box() = default;
Box(double length, double width, double height);
double volume() const;
// Getters — read-only access to private dimensions
double getLength() const { return m_length; }
double getWidth() const { return m_width; }
double getHeight() const { return m_height; }
// Setters — validated write access; negative values are silently ignored
void setLength(double length) { if (length > 0) m_length = length; }
void setWidth(double width) { if (width > 0) m_width = width; }
void setHeight(double height) { if (height > 0) m_height = height; }
private:
double m_length {1.0};
double m_width {1.0};
double m_height {1.0};
};
// Usage:
Box myBox {3.0, 4.0, 5.0};
myBox.setLength(-20.0); // ignored — m_length stays 3.0
myBox.setWidth(40.0); // accepted — m_width is now 40.0
std::println("length = {}", myBox.getLength()); // 3The naming convention is that the getter for member m_member is called getMember() and the setter is called setMember(). The one common exception is Boolean members, where the getter is typically isMember() — for example, isValid() rather than getValid().
const member functions — promising not to modify the object
A const member function is one that promises not to change any member variables of the object it is called on. You declare it by placing the const keyword after the parameter list. This matters for two reasons. First, it documents intent — readers of the class interface can immediately see which functions are read-only. Second, it allows the function to be called on const objects; without the qualifier, the compiler refuses to call it on anything declared const.
class Box {
public:
double volume() const; // const: does not change the box
double getLength() const; // const: just returns m_length
void setLength(double l); // non-const: modifies m_length
private:
double m_length {1.0}, m_width {1.0}, m_height {1.0};
};
// A const Box can only call const member functions:
const Box unitBox;
unitBox.volume(); // OK — volume() is const
unitBox.getLength(); // OK — getLength() is const
// unitBox.setLength(2.0); // ERROR — setLength modifies the object
// A non-const Box can call any member function:
Box myBox {3.0, 4.0, 5.0};
myBox.setLength(6.0); // OK
myBox.volume(); // OKThe rule is simple: mark every member function that does not modify the object as const. This is “const correctness” — a habit that pays off every time your class is stored in a const variable, passed as a const reference, or returned from a function as const.
protected: access for derived classes
The third access specifier, protected, sits between public and private. A protected member is invisible to ordinary code outside the class — just like private — but it is visible to classes that derive from (inherit from) this class. You will explore inheritance in depth in a later lesson; the key point here is why this third option exists at all.
When you design a base class that other classes will extend, you sometimes want derived classes to be able to read or modify certain internal data directly without exposing it to everyone. Making that data private would lock out derived classes; making it public would expose it to all callers. The protected specifier gives access exactly to derived classes and nobody else:
class Shape {
public:
virtual double area() const = 0; // public interface
protected:
double m_x {0.0}; // derived classes can read/write these
double m_y {0.0}; // but outside code cannot
private:
int m_id; // completely hidden, even from derived classes
};
class Circle : public Shape {
public:
Circle(double radius) : m_radius{radius} {}
double area() const override { return 3.14159 * m_radius * m_radius; }
private:
double m_radius;
// Circle can access m_x and m_y from Shape (they are protected)
// Circle cannot access m_id from Shape (it is private)
};In practice, it is often cleaner to keep member variables private and provide protected functions that derived classes can call. This gives you the same flexibility while keeping the implementation details better hidden. You will see real examples when you study inheritance.
friend: granting access to non-members
Occasionally, a free function (one that is not a member of any class) needs access to the private data of a class. Rather than making those members public, you can designate the function as a friend of the class. A friend declaration inside the class definition grants that specific function full read/write access to all members — public, protected, and private — for any object of that class passed to it.
class Box {
public:
Box() = default;
Box(double length, double width, double height);
double volume() const;
// surfaceArea is not a member, but we trust it with our private data:
friend double surfaceArea(const Box& box);
private:
double m_length {1.0}, m_width {1.0}, m_height {1.0};
};
// Friend function definition — accesses private members directly:
double surfaceArea(const Box& box) {
return 2.0 * (box.m_length * box.m_width
+ box.m_length * box.m_height
+ box.m_height * box.m_width);
}You can also declare an entire class as a friend: friend class Carton; inside Box grants every member function of Carton full access to Box's private members. Friendship is not reciprocal — Box functions do not automatically get access to Carton's private members.
Use friend sparingly. Friend declarations undermine encapsulation — they give external code a back door into private data. They are occasionally justified, most notably when implementing operator overloads that need to work symmetrically on two objects of the same class. But if you reach for friend for a routine accessor task, consider using getter functions instead.
Access specifier quick reference
| Specifier | Accessible from same class | Accessible from derived class | Accessible from outside |
|---|---|---|---|
| public | Yes | Yes | Yes |
| protected | Yes | Yes | No |
| private | Yes | No | No |
Default access: class members are private by default; struct members are public by default.
Key rules to remember
Keep member variables private — expose them only through member functions
Private data can only be changed by code you control. It lets you add validation in setters and change internal representation without breaking callers.
Put public members first, private members last
Readers of a class definition care first about the interface. Ordering public-before-private puts the most important information at the top.
Use the m_ prefix for private member variables
Inside function bodies, m_length is instantly recognisable as an instance variable, not a local or parameter. This avoids subtle shadowing bugs.
Mark every non-modifying member function as const
const-qualified functions can be called on const objects, const references, and pointers-to-const. Omitting const blocks entire usage patterns.
protected gives access to derived classes, not to the world
Use protected when a base class genuinely needs derived classes to touch certain data, but the data should still be invisible to all other code.
Use friend only as a last resort
A friend function bypasses access control entirely. Prefer accessor functions for routine access; reserve friend for operator overloading and tightly coupled helper utilities.