OO Encapsulation  «Prev  Next»
Lesson 11 Class or struct?
Objective Design a Person Class in C++

C++ Class vs struct (Designing a Person Class and Inheritance Explained)

The Person class contains members to store data and a member function to print out the data members. Classes in C++ are introduced by the keyword class. They are a form of struct whose default privacy specification is private. Thus struct and class can be used interchangeably with the appropriate access specifications. More on the C++ class can be found at the following page Class versus Struct. Lesson 9 established private and public access specifiers — this lesson applies those concepts to design a complete Person class from scratch, convert the familiar ch_stack running example from struct to class, and introduce the base class and derived class relationship that inheritance builds on.
Let us look at how we would turn our struct ch_stack into a class. First, here is the struct:

Class or struct — The Course Style Rule

The "Need to Know" Style

It will be our C++ style in this course to prefer class to struct unless all members are data members with public access. It will also be our style to use access keywords explicitly and to place public members first and private members last. We call this the "need to know" style, because everyone needs to know the public interface, but only the class provider needs to know the private implementation details. A class is an expanded concept of a data structure: instead of holding only data, it can hold both data and functions. An object is an instantiation of a class. In terms of variables, a class would be the type, and an object would be the variable.

Public Members First, Private Members Last

The "need to know" layout places the public interface at the top of the class body — the first thing a reader sees is what the class can do, not how it stores its data. The private section follows at the bottom — visible to the compiler for size calculation but not part of the contract that callers depend on. This is the opposite of the default-private layout that the class keyword produces without explicit specifiers, but it is the preferred style because it prioritizes the reader's perspective: callers read the public section; only the class implementer needs to read the private section.

When to Use struct vs class

The practical rule: use struct when all data members are public and the type carries no invariants — plain data aggregates like a coordinate pair, a color value, or a configuration record. Use class when the type enforces invariants through private data and a controlled public interface. The ch_stack type enforces that top is always within [-1, MAX_LEN-1] — it has an invariant, so class is the correct keyword. A Person type with validated age and non-empty name fields also has invariants — class is correct. A Point with public x and y fields that can be any value has no invariant — struct is appropriate.


Class Declaration Syntax

Classes are generally declared using the keyword class with the following format:

class class_name {
  access_specifier_1:
    member1;
  access_specifier_2:
    member2;
  ...
} object_names;

Where class_name is a valid identifier for the class and object_names is an optional list of names for objects of this class. The body of the declaration can contain members that are either data or function declarations, and optionally access specifiers. An access specifier is one of the following three keywords:
  1. private,
  2. public,
  3. or protected.

Designing a Person Class

Identifying Data Members and Behavior

The objective of this lesson is to design a Person class that contains members to store data and a member function to print out the data members. Applying the "need to know" style: the public section comes first and contains the interface — the constructor that initializes the object and the print() function that displays the data. The private section comes last and contains the data members — first_name_, last_name_, and age_. External code cannot read or modify these fields directly; all interaction passes through the public interface.

#include <string>
#include <string_view>
#include <iostream>
#include <stdexcept>

class Person {
 public:
    // Constructor — initializes all data members; enforces invariants
    Person(std::string_view first, std::string_view last, int age)
        : first_name_{first}, last_name_{last}, age_{age}
    {
        if (first_name_.empty()) throw std::invalid_argument("first name cannot be empty");
        if (last_name_.empty())  throw std::invalid_argument("last name cannot be empty");
        if (age_ < 0 || age_ > 150) throw std::invalid_argument("age out of range");
    }

    // Public interface — "need to know" style: interface before implementation
    void print() const {
        std::cout << "Name: " << first_name_ << ' ' << last_name_ << '\n';
        std::cout << "Age : " << age_ << '\n';
    }

    [[nodiscard]] std::string_view first_name() const noexcept { return first_name_; }
    [[nodiscard]] std::string_view last_name()  const noexcept { return last_name_;  }
    [[nodiscard]] int              age()         const noexcept { return age_;        }

 private:
    // Private data — only the class provider needs to know these details
    std::string first_name_;
    std::string last_name_;
    int         age_ = 0;
};

int main() {
    Person p{"Laura", "Smith", 28};
    p.print();                        // Name: Laura Smith  /  Age : 28
    // p.first_name_ = "hack";        // compile error: first_name_ is private
}


The print() Member Function


Why class Instead of struct

The Person class enforces three invariants: first name is non-empty, last name is non-empty, and age is in [0, 150]. These invariants are maintained by the constructor, which throws if any condition is violated. Because the data members are private, no external code can bypass these checks — it is impossible to create a Person with an empty name or an age of -5. If Person were a struct with public fields, any code in the program could write p.age_ = -999 with no compile error and no runtime check. The invariants exist because the keyword is class and the data is private.

Converting ch_stack from struct to class

The ch_stack running example has appeared throughout this module as a struct. The following two code blocks show the struct version and the class version side by side. Both are stripped of the legacy syntax highlighting spans and modernized to C++23 conventions:

#include <cassert>

static constexpr int MAX_LEN = 40;

struct ch_stack {
    // data representation — public by default in struct
    char s[MAX_LEN];
    int  top = -1;
    enum class Sentinel : int { EMPTY = -1, FULL = MAX_LEN - 1 };

    // operations represented as member functions
    void reset() noexcept { top = static_cast<int>(Sentinel::EMPTY); }
    void push(char c) {
        assert(top != static_cast<int>(Sentinel::FULL));
        s[++top] = c;
    }
    [[nodiscard]] char pop() {
        assert(top != static_cast<int>(Sentinel::EMPTY));
        return s[top--];
    }
    [[nodiscard]] char top_of() const {
        assert(top != static_cast<int>(Sentinel::EMPTY));
        return s[top];
    }
    [[nodiscard]] bool empty() const noexcept {
        return top == static_cast<int>(Sentinel::EMPTY);
    }
    [[nodiscard]] bool full() const noexcept {
        return top == static_cast<int>(Sentinel::FULL);
    }
};

#include <cassert>

static constexpr int MAX_LEN = 40;

class ch_stack {
    // data representation — private by default in class
    char s[MAX_LEN];
    int  top = -1;
    enum class Sentinel : int { EMPTY = -1, FULL = MAX_LEN - 1 };

 public:
    // operations represented as member functions — explicitly public
    void reset() noexcept { top = static_cast<int>(Sentinel::EMPTY); }
    void push(char c) {
        assert(top != static_cast<int>(Sentinel::FULL));
        s[++top] = c;
    }
    [[nodiscard]] char pop() {
        assert(top != static_cast<int>(Sentinel::EMPTY));
        return s[top--];
    }
    [[nodiscard]] char top_of() const {
        assert(top != static_cast<int>(Sentinel::EMPTY));
        return s[top];
    }
    [[nodiscard]] bool empty() const noexcept {
        return top == static_cast<int>(Sentinel::EMPTY);
    }
    [[nodiscard]] bool full() const noexcept {
        return top == static_cast<int>(Sentinel::FULL);
    }
};

The Only Syntax Difference

The only difference is the keyword class, which is used in place of struct. Every member declaration, every function signature, every access specifier — identical. The public: label is present in both versions; it is required in the class version to override the default-private access, and it is optional but present in the struct version to make the intent explicit per the "need to know" style rule.

The Semantic Difference — Default Access

The semantic difference is the default access level. In a struct, members declared before the first access specifier are public by default — the data members s[], top, and the Sentinel enum are publicly accessible without the public: label. In a class, those same members declared before the first access specifier are private by default — they are inaccessible to external code until the public: label overrides the default for the member functions. The "need to know" style makes this distinction irrelevant in practice by always using explicit access specifiers — but understanding the default is essential for reading legacy code.


Base Classes and Derived Classes

The is-a Relationship

Often, an object of one class is an object of another class as well. For example, in geometry, a rectangle is a quadrilateral (as are squares, parallelograms, and trapezoids). Thus, in C++, class Rectangle can be said to inherit from class Quadrilateral. In this context, class Quadrilateral is a base class and class Rectangle is a derived class. A rectangle is a specific type of quadrilateral, but it is incorrect to claim that a quadrilateral is a rectangle — the quadrilateral could be a parallelogram or some other shape. The "is-a" relationship is the fundamental test for whether inheritance is appropriate: a Rectangle is-a Quadrilateral ✓. A Car is-a Vehicle ✓. A Stack is-a Array ✗ — a stack uses an array internally but is not an array; it would be misrepresented by inheritance.

Figure 2-11 — Inheritance Examples

Figure 2-11 lists several simple examples of base classes and derived classes. Each row represents a valid is-a relationship — a GraduateStudent is-a Student, a Circle is-a Shape, a MortgageLoan is-a Loan:
Figure 2-11: Inheritance examples
Base class Derived classes
Student GraduateStudent, UndergraduateStudent
Shape Circle, Triangle, Rectangle, Sphere, Cube
Loan CarLoan, HomeImprovementLoan, MortgageLoan
Employee Faculty, Staff
Account CheckingAccount, SavingsAccount

When Inheritance Is Appropriate

In C++, inheritance is expressed using the colon syntax: class Rectangle : public Quadrilateral { ... };. The derived class inherits all public and protected members of the base class and can add its own members or override virtual functions with specialized behavior. Modern C++ uses the override keyword on overriding functions to instruct the compiler to verify the override is valid — preventing the common error of a mismatched signature that silently creates a new function instead of overriding the intended one. The final keyword prevents further derivation from a class or prevents a specific virtual function from being overridden in further derived classes.

Derived-Class Objects and Inheritance Hierarchies

The Set of Objects a Base Class Represents

Because every derived-class object is an object of its base class, and one base class can have many derived classes, the set of objects represented by a base class typically is larger than the set of objects represented by any of its derived classes. For example, the base class Vehicle represents all vehicles, including cars, trucks, boats, airplanes, and bicycles. By contrast, derived class Car represents a smaller, more specific subset of all vehicles.

Treelike Hierarchical Structures

Inheritance relationships form treelike hierarchical structures. A base class exists in a hierarchical relationship with its derived classes. Although classes can exist independently, once they are employed in inheritance relationships, they become affiliated with other classes. A class becomes either a base class, supplying members to other classes, a derived class, inheriting its members from other classes, or both. Let us develop a simple inheritance hierarchy with five levels, represented by the UML class diagram in Fig. 2-12. A university community has thousands of members.

Inheritance hierarchy for university CommunityMembers
Figure 2-12: Inheritance hierarchy for university CommunityMembers

The University Community Hierarchy — Five Levels

These members consist of employees, students, and alumni. Employees are either faculty members or staff members. Faculty members are either administrators (such as deans and department chairpersons) or teachers. Some administrators, however, also teach. The diagram shows five levels of single inheritance from CommunityMember down to Administrator and Teacher, with one case of multiple inheritance at the bottom.

Multiple Inheritance — AdministratorTeacher

The AdministratorTeacher class at the bottom of the hierarchy inherits from both Administrator and Teacher — this is multiple inheritance, where a derived class has more than one direct base class. C++ supports multiple inheritance, making it one of the few major languages to do so. The AdministratorTeacher case is the classic justification: an administrator who also teaches genuinely is-a both an Administrator and a Teacher — two separate is-a relationships hold simultaneously. Multiple inheritance introduces complexity — particularly the diamond problem when two base classes share a common ancestor — which C++ addresses through virtual base classes. The university community hierarchy makes this complexity visible and motivated before the syntax is introduced in later modules.

Person Class Exercise

Click the Exercise link below to design a class called Person that contains members to store data and a member function to print out the data members.
Person Class - Exercise
In the next lesson, the module concludes with a summary of the OOP encapsulation concepts covered — from member function declaration through class design and inheritance hierarchies.

SEMrush Software