Skip to content

Classes, Structs & Encapsulation

Understand the difference between struct and class, access specifiers, encapsulation, const member functions, static members, and modern aggregate initialization.

What Is a Class?

A class in C++ is a user-defined type that bundles data (member variables) and operations on that data (member functions) into a single unit. This bundling is called encapsulation — one of the four pillars of OOP.

The key insight is that a class defines an invariant: a set of rules that must always be true about its internal state. For example, a Date class might require that the month is always between 1 and 12. Encapsulation lets you enforce these invariants by controlling access to internal state.

struct vs class

In C++, struct and class are almost identical. The only difference is the default access specifier: struct defaults to public, while class defaults to private. Convention: use struct for plain data aggregates, class when you have invariants to maintain.

struct_vs_class.cpp
#include <iostream>
#include <string>

// struct: members are public by default
struct Point {
    double x;  // public
    double y;  // public
};

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

public:
    BankAccount(const std::string& name, double initial)
        : owner{name}, balance{initial} {}

    double get_balance() const { return balance; }

    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
};

int main() {
    Point p{3.0, 4.0};  // aggregate initialization
    std::cout << p.x << ", " << p.y << '\n';

    BankAccount acct{"Alice", 1000.0};
    acct.deposit(500.0);
    std::cout << acct.get_balance() << '\n';  // 1500
    // acct.balance = -999;  // ERROR: private member

    return 0;
}

Access Specifiers & the this Pointer

C++ provides three access levels: public (anyone can access), private (only the class and its friends), and protected (the class, its friends, and derived classes). The this pointer is an implicit pointer to the current object available inside every non-static member function.

access_specifiers.cpp
#include <iostream>
#include <string>

class Employee {
private:
    std::string name_;
    int id_;

protected:
    double salary_;  // accessible to derived classes

public:
    Employee(const std::string& name, int id, double salary)
        : name_{name}, id_{id}, salary_{salary} {}

    // const member function: promises not to modify the object
    const std::string& get_name() const { return name_; }
    int get_id() const { return id_; }

    // 'this' pointer usage — return *this for method chaining
    Employee& set_name(const std::string& name) {
        this->name_ = name;  // 'this->' disambiguates
        return *this;
    }

    Employee& set_id(int id) {
        this->id_ = id;
        return *this;
    }

    void print() const {
        std::cout << "Employee{" << name_ << ", #" << id_
                  << ", $" << salary_ << "}\n";
    }
};

int main() {
    Employee e{"Bob", 42, 75000.0};
    e.set_name("Robert").set_id(43);  // method chaining via *this
    e.print();  // Employee{Robert, #43, $75000}
    return 0;
}

Static Members & friend

Static members belong to the class, not to any instance. Use them for shared state or utility functions. The friend keyword grants a non-member function or another class access to private members — use sparingly, as it breaks encapsulation.

static_friend.cpp
#include <iostream>

class Widget {
    int value_;

    // C++17: inline static — define in header, no .cpp needed
    inline static int count_ = 0;

public:
    explicit Widget(int v) : value_{v} { ++count_; }
    ~Widget() { --count_; }

    static int get_count() { return count_; }

    // friend function can access private members
    friend std::ostream& operator<<(std::ostream& os, const Widget& w);
};

std::ostream& operator<<(std::ostream& os, const Widget& w) {
    return os << "Widget(" << w.value_ << ')';  // accesses private value_
}

int main() {
    Widget a{10}, b{20};
    std::cout << Widget::get_count() << '\n';  // 2
    std::cout << a << '\n';  // Widget(10)
    {
        Widget c{30};
        std::cout << Widget::get_count() << '\n';  // 3
    }
    std::cout << Widget::get_count() << '\n';  // 2
    return 0;
}

Aggregate Initialization & Designated Initializers (C++20)

An aggregate is a class or struct with no user-declared constructors, no private/protected non-static data members, no virtual functions, and no virtual base classes. C++20 adds designated initializers, letting you name which members you are initializing.

designated_init.cpp
#include <iostream>
#include <string>

struct Config {
    std::string host = "localhost";
    int port = 8080;
    bool use_tls = false;
    int max_connections = 100;
};

int main() {
    // Traditional aggregate initialization
    Config c1{"example.com", 443, true, 200};

    // C++20 designated initializers — much more readable
    Config c2{
        .host = "api.example.com",
        .port = 443,
        .use_tls = true
        // .max_connections uses default value 100
    };

    std::cout << c2.host << ':' << c2.port << '\n';
    std::cout << "TLS: " << std::boolalpha << c2.use_tls << '\n';
    std::cout << "Max: " << c2.max_connections << '\n';  // 100
    return 0;
}
Best Practice

Make data members private by default and expose only what is needed through a minimal public interface. If a class has no invariant to maintain (it's just a bundle of data), use a struct with public members instead. Do not add getters and setters for every field — that defeats the purpose of encapsulation. Instead, provide meaningful operations like deposit() and withdraw() rather than set_balance().

Pitfall

If a member function does not modify the object's state, mark it const. Without const, you cannot call the function on a const object or through a const reference — which happens all the time when passing objects by const&. A missing const on an accessor is a common source of compilation errors in larger codebases.

Key Takeaways
  • struct and class differ only in default access: struct is public, class is private
  • Encapsulation protects class invariants — use private data with a meaningful public interface
  • Mark non-mutating member functions const so they work with const references
  • Use inline static (C++17) for static members defined in headers
  • C++20 designated initializers make aggregate initialization readable and self-documenting
  • friend should be used sparingly — typically for operator overloads that need private access

Quiz — Test Your Knowledge

(15 XP)

1. What is the ONLY difference between `struct` and `class` in C++?

2. Why should you mark accessor member functions as `const`?

3. What does `inline static int count_ = 0;` achieve in C++17?