Public and Private Members and Access Specifiers

Classes let you control who can access their members using access specifiers.

The three access specifiers

C++ provides three access levels:

  • public: Accessible from anywhere
  • private: Accessible only within the class
  • protected: Accessible within the class and derived classes (covered later)

Public members

Public members are accessible to everyone:

class Character
{
public:
    int health{};

    void takeDamage(int amount)
    {
        health -= amount;
    }
};

int main()
{
    Character hero{};
    hero.health = 100;       // OK: public member variable
    hero.takeDamage(25);     // OK: public member function

    return 0;
}

Anyone with access to a Character object can read or modify health and call takeDamage().

Private members

Private members are accessible only within the class itself:

class Wallet
{
private:
    int gold{};

public:
    void addGold(int amount)
    {
        gold += amount;  // OK: inside the class
    }

    int getGold()
    {
        return gold;  // OK: inside the class
    }
};

int main()
{
    Wallet playerWallet{};
    playerWallet.addGold(100);          // OK: public function
    std::cout << playerWallet.getGold();  // OK: public function
    playerWallet.gold = 9999999;        // Error: gold is private!

    return 0;
}

Direct access to gold from outside the class is forbidden. You must use the public interface (addGold(), getGold()).

Default access levels

In classes: Members are private by default.

class Example
{
    int privateByDefault{};  // private

public:
    int explicitlyPublic{};  // public
};

In structs: Members are public by default.

struct Data
{
    int publicByDefault{};  // public

private:
    int explicitlyPrivate{};  // private
};
Best Practice
Be explicit with access specifiers for clarity.

Why use private members?

Data protection: Prevent invalid states.

class Gauge
{
private:
    int value{};  // Always 0-100

public:
    void setValue(int v)
    {
        if (v >= 0 && v <= 100)
            value = v;
    }

    int getValue() const
    {
        return value;
    }
};

Making value private ensures it can't be set outside the 0-100 range. The setValue() function validates input.

Implementation hiding: Change internals without breaking code that uses the class.

// Version 1
class Cache
{
private:
    int data[50]{};
    int count{};

public:
    void store(int value);
    int retrieve(int index);
};

// Version 2 - changed implementation
class Cache
{
private:
    std::vector<int> data{};  // Changed to vector

public:
    void store(int value);    // Interface unchanged!
    int retrieve(int index);
};

Users of Cache don't need to change their code when you change the internal implementation.

Controlled access: Perform actions when values change.

class Monitor
{
private:
    int failureCount{};

public:
    void recordFailure()
    {
        ++failureCount;
        if (failureCount > 10)
            triggerAlarm();
    }

    int getFailureCount() const
    {
        return failureCount;
    }

private:
    void triggerAlarm();
};

By controlling access to failureCount, you can trigger alarms when failures exceed a threshold.

Naming conventions

Many teams prefix private members with m_ (member) or append _:

class Example
{
private:
    int m_health{};     // Prefix style
    double damage_{};   // Suffix style

public:
    int getHealth() const { return m_health; }
    void setHealth(int h) { m_health = h; }
};

Choose a convention and apply it consistently.

Member access in practice

class Weapon
{
private:
    int damage{};
    int durability{};

public:
    // Setters validate input
    void setDamage(int d)
    {
        if (d > 0)
            damage = d;
    }

    void setDurability(int dur)
    {
        if (dur >= 0)
            durability = dur;
    }

    // Getters provide read access
    int getDamage() const { return damage; }
    int getDurability() const { return durability; }

    // Computed properties
    int criticalDamage() const
    {
        return damage * 2;
    }

    bool isBroken() const
    {
        return durability == 0;
    }
};

This design:

  • Protects data from invalid values
  • Provides a clean public interface
  • Can change implementation without affecting users
  • Computes derived values on demand
Best Practice
Keep data members private and expose them through public getters/setters only when needed. Make the interface minimal by only exposing what users need. Validate in setters to check that values are valid before storing them. Use const for getters since they typically don't modify the object.

Summary

Access specifiers: C++ provides three access levels - public (accessible from anywhere), private (accessible only within the class), and protected (accessible within the class and derived classes).

Default access levels: In classes, members are private by default. In structs, members are public by default. Being explicit with access specifiers improves code clarity.

Data protection: Private members prevent invalid states by controlling access through public member functions that can validate input and enforce invariants.

Implementation hiding: Private members allow you to change internal implementation without breaking code that uses the class, as long as the public interface remains the same.

Controlled access: Using private members with public access functions lets you perform additional actions when values change, such as validation, logging, or triggering side effects.

Access specifiers are fundamental to encapsulation, one of the core principles of object-oriented programming. By controlling member visibility, you create clean public interfaces while protecting implementation details and maintaining object integrity.