Skip to content

Classes and Structs

Video: Object Oriented Programming (OOP) in C++ Course

Object-Oriented Programming (OOP) is a fundamental paradigm in C++ that allows you to organize code into logical units called classes and structs. These user-defined types encapsulate data and behavior, providing a powerful way to model real-world entities and create maintainable, reusable code.

What are Classes and Structs?

Classes and structs are user-defined types that group related data and functions together. They serve as blueprints for creating objects - instances of the class or struct that contain actual data values.

Key Concepts:

  • Encapsulation: Bundling data and methods that operate on that data
  • Data Abstraction: Hiding complex implementation details behind a simple interface
  • Access Control: Managing how class members can be accessed and modified
  • Object Creation: Instantiating classes to create objects with specific data

Classes vs Structs: The Key Difference

The main difference between classes and structs in C++ is the default access level:

cpp
// Class: members are private by default
class BankAccount {
    double balance;        // private by default
    std::string owner;     // private by default

public:
    void deposit(double amount);  // explicitly public
    double getBalance() const;    // explicitly public
};

// Struct: members are public by default
struct Point {
    int x;                // public by default
    int y;                // public by default

private:
    void validate();      // explicitly private
};

Important: This is the only difference! Both classes and structs can:

  • Contain any type of member (data, functions, nested types)
  • Support inheritance and polymorphism
  • Have constructors, destructors, and member functions
  • Use all access specifiers

The only extra difference, for the pedantic folks out there, is that the default inheritance for structs is public, while the default inheritance for classes is private.

Access Specifiers

Access specifiers control the visibility and accessibility of class members. They enforce encapsulation by restricting how external code can interact with class internals.

Three Access Levels:

cpp
class Example {
private:
    // Private members: accessible ONLY within this class
    int privateData;
    void privateMethod() { /* ... */ }

protected:
    // Protected members: accessible within this class AND derived classes
    int protectedData;
    void protectedMethod() { /* ... */ }

public:
    // Public members: accessible EVERYWHERE
    int publicData;
    void publicMethod() { /* ... */ }
};

1. Private Access (Default for Classes)

cpp
class BankAccount {
private:
    double balance;           // Private data member
    std::string accountNumber; // Private data member

    void validateAmount(double amount) {  // Private helper method
        if (amount < 0) {
            throw std::invalid_argument("Amount cannot be negative");
        }
    }

public:
    void deposit(double amount) {
        validateAmount(amount);  // Can access private method
        balance += amount;
    }

    double getBalance() const {
        return balance;  // Can access private data
    }
};

// Usage
BankAccount account;
account.deposit(100.0);        // OK: public method
double bal = account.getBalance(); // OK: public method
// account.balance = 1000.0;   // ERROR: private member
// account.validateAmount(50); // ERROR: private method

Key Points:

  • Default access level for classes
  • Only accessible within the class
  • Enforces encapsulation - external code cannot modify internal state
  • Helper methods are often private

2. Protected Access

cpp
class BankAccount {
protected:
    double balance;           // Protected data member
    std::string accountNumber; // Protected data member

    void validateAmount(double amount) {  // Protected method
        if (amount < 0) {
            throw std::invalid_argument("Amount cannot be negative");
        }
    }

public:
    virtual void deposit(double amount) {
        validateAmount(amount);
        balance += amount;
    }
};

class SavingsAccount : public BankAccount {
public:
    void addInterest(double rate) {
        // Can access protected members from base class
        double interest = balance * rate;
        balance += interest;  // OK: balance is protected
        validateAmount(interest);  // OK: method is protected
    }

    void transferTo(BankAccount& other, double amount) {
        validateAmount(amount);
        if (balance >= amount) {
            balance -= amount;  // OK: can access protected balance
            other.deposit(amount);  // OK: deposit is public
        }
    }
};

// Usage
SavingsAccount savings;
savings.addInterest(0.05);  // OK: public method
// savings.balance = 1000;   // ERROR: protected member (not accessible from outside)

Key Points:

  • Accessible within the class AND derived classes
  • Useful for inheritance - allows derived classes to access base class internals
  • Still maintains encapsulation from external code
  • Common for data members that derived classes need to access

3. Public Access (Default for Structs)

cpp
class BankAccount {
public:
    double balance;           // Public data member
    std::string accountNumber; // Public data member

    void deposit(double amount) {
        if (amount < 0) {
            throw std::invalid_argument("Amount cannot be negative");
        }
        balance += amount;
    }

    double getBalance() const {
        return balance;
    }
};

// Usage
BankAccount account;
account.balance = 1000.0;     // OK: public member
account.accountNumber = "123"; // OK: public member
account.deposit(500.0);       // OK: public method

Key Points:

  • Accessible everywhere
  • Default access level for structs
  • Part of the public interface - external code can access and modify
  • Use carefully - public data members can break encapsulation

Using Access Specifiers Effectively

Private: Use for Internal Implementation

cpp
class Vector3D {
private:
    double x, y, z;  // Internal data representation

    void normalize() {  // Internal helper method
        double length = std::sqrt(x*x + y*y + z*z);
        if (length > 0) {
            x /= length;
            y /= length;
            z /= length;
        }
    }

public:
    Vector3D(double x, double y, double z) : x(x), y(y), z(z) {}

    void setLength(double length) {
        normalize();  // Use private helper
        x *= length;
        y *= length;
        z *= length;
    }

    double getX() const { return x; }
    double getY() const { return y; }
    double getZ() const { return z; }
};

Use private for:

  • Data members that shouldn't be directly modified
  • Helper methods that are implementation details
  • Internal state that needs protection

Protected: Use for Inheritance Support

cpp
class Shape {
protected:
    double area;  // Derived classes need to access this

    virtual void calculateArea() = 0;  // Derived classes must implement

public:
    virtual double getArea() const { return area; }
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {
        calculateArea();  // Can call protected method
    }

protected:
    void calculateArea() override {
        area = M_PI * radius * radius;  // Can access protected area
    }
};

Use protected for:

  • Data members that derived classes need to access
  • Methods that derived classes should override
  • Implementation details shared with inheritance hierarchy

Public: Use for External Interface

cpp
class BankAccount {
public:
    // Constructor - part of public interface
    BankAccount(const std::string& owner, double initialBalance);

    // Public methods - external interface
    void deposit(double amount);
    bool withdraw(double amount);
    double getBalance() const;
    std::string getOwner() const;

    // Destructor - part of public interface
    ~BankAccount();

private:
    // Internal implementation
    double balance;
    std::string owner;
    std::vector<Transaction> transactionHistory;

    void logTransaction(const Transaction& t);
    bool validateWithdrawal(double amount);
};

Use public for:

  • Constructor and destructor
  • Methods that external code should call
  • Interface that defines how the class is used
  • Constants that should be accessible

When to Use Classes vs Structs

Use Structs When:

cpp
// Simple data containers
struct Point2D {
    double x, y;  // Public by default - simple access

    Point2D(double x, double y) : x(x), y(y) {}

    double distanceTo(const Point2D& other) const {
        double dx = x - other.x;
        double dy = y - other.y;
        return std::sqrt(dx*dx + dy*dy);
    }
};

// POD (Plain Old Data) types
struct Employee {
    std::string name;
    int id;
    double salary;
    std::string department;
};

// Configuration structures
struct GameSettings {
    int screenWidth = 1920;
    int screenHeight = 1080;
    bool fullscreen = false;
    double volume = 1.0;
    std::string language = "English";
};

Choose structs for:

  • Simple data containers with no complex behavior
  • POD types that need to be compatible with C code
  • Configuration objects where all data should be accessible
  • When you want public access by default

Use Classes When:

cpp
class BankAccount {
private:
    double balance;
    std::string accountNumber;
    std::vector<Transaction> history;

    void validateAmount(double amount);
    void logTransaction(const Transaction& t);

public:
    BankAccount(const std::string& accountNum, double initialBalance);

    void deposit(double amount);
    bool withdraw(double amount);
    double getBalance() const;

    ~BankAccount();
};

Choose classes for:

  • Complex objects with behavior and state
  • When you want encapsulation by default
  • Objects that need to maintain invariants
  • When you want private access by default

Best Practices

1. Start with Private, Make Public as Needed

cpp
class TemperatureSensor {
private:
    double currentTemp;
    double minTemp;
    double maxTemp;

    void validateTemperature(double temp);
    void logTemperatureChange(double oldTemp, double newTemp);

public:
    TemperatureSensor(double min, double max);

    double getTemperature() const { return currentTemp; }
    bool setTemperature(double temp);
    bool isInRange() const;
};

2. Use Getters and Setters for Data Access

cpp
class Person {
private:
    std::string name;
    int age;

public:
    // Getters
    const std::string& getName() const { return name; }
    int getAge() const { return age; }

    // Setters with validation
    void setName(const std::string& newName) {
        if (!newName.empty()) {
            name = newName;
        }
    }

    void setAge(int newAge) {
        if (newAge >= 0 && newAge <= 150) {
            age = newAge;
        }
    }
};

3. Keep Public Interface Minimal

cpp
class DatabaseConnection {
private:
    std::string connectionString;
    bool isConnected;
    std::mutex connectionMutex;

    void establishConnection();
    void validateConnection();
    void logConnectionAttempt(bool success);

public:
    DatabaseConnection(const std::string& connStr);

    bool connect();
    void disconnect();
    bool executeQuery(const std::string& query);

    ~DatabaseConnection();
};

4. Use Protected for Inheritance

cpp
class BaseClass {
protected:
    int protectedData;

    virtual void protectedMethod() {
        // Implementation that derived classes can override
    }

public:
    virtual void publicMethod() {
        protectedMethod();  // Call protected method
    }
};

class DerivedClass : public BaseClass {
public:
    void derivedMethod() {
        protectedData = 42;      // Can access protected data
        protectedMethod();       // Can call protected method
    }
};

Summary

Classes and structs are the foundation of object-oriented programming in C++:

Key Differences:

  • Classes: Private members by default, use for encapsulation
  • Structs: Public members by default, use for simple data containers

Access Specifiers:

  • Private: Only accessible within the class (default for classes)
  • Protected: Accessible within class and derived classes
  • Public: Accessible everywhere (default for structs)

Best Practices:

  • Start with private and make public as needed
  • Use classes when you want encapsulation by default
  • Use structs when you want public access by default
  • Keep public interface minimal and well-designed
  • Use protected for inheritance scenarios

When to Use Each:

  • Structs: Simple data containers, POD types, configuration objects
  • Classes: Complex objects with behavior, when encapsulation is needed
  • Private: Internal implementation details
  • Protected: Members that derived classes need to access
  • Public: External interface and constructors/destructors

Understanding these concepts provides the foundation for building robust, maintainable C++ applications using object-oriented principles.

Questions

Q: What is the main difference between a class and a struct in C++?

The main difference is the default access level. In a class, members are private by default, while in a struct, members are public by default. This is the only difference - both can contain any type of member, support inheritance, and have the same functionality.

Q: What happens when you try to access a private member from outside the class?

Accessing a private member from outside the class causes a compilation error. The compiler enforces access control at compile time, preventing you from violating encapsulation. This is a fundamental feature of C++ that helps maintain data integrity and design intent.

Q: Which access specifier allows derived classes to access members but prevents external access?

The protected access specifier allows derived classes to access members while preventing external access. This provides a middle ground between public (accessible everywhere) and private (accessible only within the class), making it ideal for inheritance scenarios.

Q: What is the purpose of access specifiers in C++?

Access specifiers enforce encapsulation by controlling how class members can be accessed. They help maintain data integrity, prevent unauthorized access to internal state, and make the class interface clear and predictable. This is a fundamental principle of object-oriented programming.

Q: When should you use a struct instead of a class in C++?

Use a struct when you want all members to be public by default, typically for simple data containers or POD (Plain Old Data) types. Use a class when you want to enforce encapsulation with private members by default. The choice is mainly about expressing intent and default behavior.