Classes and Objects

Public, Private, and Protected Access

Classes can decide to show any of their members to the rest of the program, and to hide others from them. There are three keywords that declare whether a particular member is to be treated this way: the keywords public, private, and protected. Each keyword affects all members from that point on until the end of the class body, or until another keyword starts.

Members declared public can be accessed by any code that can see the object. That is how we have done things up until now. Members declared private can be accessed by none but code internal to the class itself.

Members declared protected can be accessed in the same way as private members by external code; however, they are accessible to classes which inherit from this one.

Inheritance is covered much later; the point to know at this stage is that protected access exists. The rules for it are very similar to private access but also include derived classes.

Every class defaults to private access. If there is no access specifier present when a class is declared, then by default every member is public. So to make the private members accessible outside of the class, we must write public:, etc.

In direct contrast to class, structs' default is public.

This difference between classes and structs is basically what leads most programmers to use class for hideable data, and struct for things that aren't.

Hiding data has important uses; unrestricted access can cause the program to get an object into a state where it is no longer meaningful. Imagine the class to store a fraction in:

#include <iostream>

// Define the members that belong to this type
class Fraction
{
public:
    int numerator{};
    int denominator{};
};

int main()
{
    Fraction f{};
    f.numerator = 3;
    f.denominator = 0;

    std::cout << f.numerator << '/' << f.denominator << '\n';

    return 0;
}

Here when object f is created both numerator and denominator are initialized to 0, the two assignment lines then make the numerator 3, and the denominator 0, without complaint from the program; however, a fraction with 0 on the bottom line is, as you know, meaningless. The program then proceeds, and the print line runs, printing 3/0. The class cannot protect itself, as all its members are public.

But look what happens when the members are declared to be private:

#include <iostream>

class Fraction
{
private:
    int numerator{};
    int denominator{};
};

int main()
{
    Fraction f{};

    // f.numerator = 3;   // Compile error: numerator is private
    // f.denominator = 0; // Compile error: denominator is private

    return 0;
}

Any call from the main function attempting to access either numerator or denominator results in a compile-time error. The compiler enforces the rules, no runtime check can be performed, and you cannot break what isn't visible to you anyway, in the first place. The object still can be created, but the state of its members cannot be changed from outside the class.

The state of such an object cannot be mutated, of course, so we have the need for an object where data must be private and the necessary manipulations of it have to be handled by public member functions. The following class is written so:

#include <iostream>

class Fraction
{
private:
    int numerator{};
    int denominator{ 1 };

public:
    void set(int n, int d)
    {
        if (d == 0)
        {
            // Exit the function early when this condition is met
            return;
        }

        numerator = n;
        denominator = d;
    }

    void print()
    {
        std::cout << numerator << '/' << denominator << '\n';
    }
};

int main()
{
    Fraction f{};

    f.set(3, 4);
    f.print();

    f.set(5, 0); // Ignored because denominator would be zero
    f.print();

    return 0;
}

The two members numerator and denominator are kept private, so they are not accessible from main. The public member function set accepts parameters for the numerator and denominator and, only if the denominator is non-zero, will it update the appropriate members of the class. The public member function print is then called to print the numerator and denominator, after being fetched from members, as both are private members, so printing should be from members too. Outsider does not know anything, only the public functions it is being allowed to call.

This technique is also useful if a class must maintain certain invariants.

 For instance, a class simulating a bank account could keep the balance private and offer only deposit and withdraw functions. The deposit function could refuse negative values and withdraw could prevent the balance from becoming negative.

Exercise: Public, Private, and Protected AccessStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Public, Private, and Protected Access

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

Change the second Fraction::set call to use denominator 2 instead of 0. Predict both prints and explain why the public setter accepts this new value.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

#include <iostream>

class Fraction
{
private:
    int numerator{};
    int denominator{ 1 };

public:
    void set(int n, int d)
    {
        if (d == 0)
        {
            // Exit the function early when this condition is met
            return;
        }

        numerator = n;
        denominator = d;
    }

    void print()
    {
        std::cout << numerator << '/' << denominator << '\n';
    }
};

int main()
{
    Fraction f{};

    f.set(3, 4);
    f.print();

    f.set(5, 0); // Ignored because denominator would be zero
    f.print();

    return 0;
}

Hints

Hint 1

Fraction hides numerator and denominator, so main changes them through set. Inspect the d == 0 check before predicting the second f.print().

Hint 2

Change only the second denominator argument to 2. Follow the d == 0 guard to decide whether the subsequent member assignments now run.

Worked solution

The second setter call now uses denominator 2 instead of 0. It passes the validation and updates both private members, so the second output is 5/2 instead of repeating 3/4.

#include <iostream>

class Fraction
{
private:
    int numerator{};
    int denominator{ 1 };

public:
    void set(int n, int d)
    {
        if (d == 0)
        {
            // Exit the function early when this condition is met
            return;
        }

        numerator = n;
        denominator = d;
    }

    void print()
    {
        std::cout << numerator << '/' << denominator << '\n';
    }
};

int main()
{
    Fraction f{};

    f.set(3, 4);
    f.print();

    f.set(5, 2);
    f.print();

    return 0;
}

Example output

3/4
5/2

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.