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:
private,
public,
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
print() is declared const — it reads the data members without modifying them, so it can be called on const Person objects and through const Person& references. The function outputs directly to std::cout using '\n' rather than std::endl — '\n' writes a newline character without flushing the output buffer, which is more efficient for multiple consecutive outputs. The three accessor functions — first_name(), last_name(), age() — are marked [[nodiscard]] and noexcept, following the const-correctness and attribute conventions established in lessons 7 and 9.
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.
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.