Classes & Objects

C++ · Lesson 13 of 32 7 min read Updated

So far your data has lived in separate variables: a name here, a roll number there, marks somewhere else. Real things have several pieces of data that belong together, and rules about how that data may change. A class groups data and the functions that work on it into one new type. Each variable of that type is an object.

Classes are the heart of object-oriented programming (OOP) in C++. In this lesson you will create simple data types with struct, protect data with private members in a class, write member functions, and split a class across header and source files.

The simplest class is a struct whose members are all public. Each member has a type and a name, and can have a default value.

C++
#include <print>
#include <string>

struct Student {
    std::string name;
    int roll = 0;
    double percentage = 0.0;
};

int main() {
    Student a{"Aarav", 12, 86.5};
    Student b{.name = "Isha", .roll = 7, .percentage = 91.0};   // C++20

    b.percentage += 1.5;
    Student c = a;                 // copies every member
    c.name = "Aarav's copy";

    std::println("{} (roll {}) scored {}%", a.name, a.roll, a.percentage);
    std::println("{} (roll {}) scored {}%", b.name, b.roll, b.percentage);
    std::println("{}", c.name);
}
Output
Aarav (roll 12) scored 86.5%
Isha (roll 7) scored 92.5%
Aarav's copy

You reach members with a dot: b.percentage. The second object uses designated initializers (C++20), which name each member and make long initialisations readable. Notice that c = a makes a full, independent copy: changing c.name does not touch a. C++ objects behave like values, unlike Java or Python objects, which are shared through references.

class: data with rules

A bank balance should never be changed directly by any code that feels like it; deposits must be positive, and withdrawals must not exceed the balance. A class lets you hide the data behind access specifiers:

  • public: members can be used by any code.
  • private: members can be used only by the class's own member functions.
C++
#include <print>

class BankAccount {
public:
    void deposit(long long amount) {
        if (amount <= 0) {
            std::println("Deposit must be positive");
            return;
        }
        balance_ += amount;
    }

    bool withdraw(long long amount) {
        if (amount > balance_) {
            return false;
        }
        balance_ -= amount;
        return true;
    }

    long long balance() const { return balance_; }

private:
    long long balance_ = 0;        // whole rupees
};

int main() {
    BankAccount account;
    account.deposit(5000);
    account.deposit(-20);
    bool ok = account.withdraw(1200);
    bool too_much = account.withdraw(10000);

    std::println("Withdraw 1200: {}, withdraw 10000: {}", ok, too_much);
    std::println("Balance: Rs. {}", account.balance());
    // account.balance_ = 1'000'000;    // error: 'balance_' is private
}
Output
Deposit must be positive
Withdraw 1200: true, withdraw 10000: false
Balance: Rs. 3800

Hiding data and allowing changes only through checked member functions is called encapsulation. The rule "the balance is never negative" is called an invariant, and the class guarantees it. Many codebases mark private members with a trailing underscore, as in balance_, so they are easy to recognise.

The only technical difference between struct and class is the default access: struct members are public by default, class members private. By convention, use struct for simple bundles of data and class when there are rules to protect.

Member functions and const

Functions declared inside a class are member functions (also called methods). Inside them, you can use the members directly, and the hidden pointer this points to the current object.

A member function marked const after its brackets promises not to change the object. Only const member functions can be called on a const object, so mark every function that only reads.

C++
#include <print>

struct Rectangle {
    double width = 0;
    double height = 0;

    double area() const;               // declared here, defined below
    void scale(double factor);
};

double Rectangle::area() const {
    return width * height;
}

void Rectangle::scale(double factor) {
    this->width *= factor;             // same as width *= factor
    height *= factor;
}

int main() {
    Rectangle room{4, 2.5};
    room.scale(2);
    std::println("Room: {} x {} = {} sq m", room.width, room.height, room.area());

    const Rectangle plot{10, 20};
    std::println("Plot area: {} sq m", plot.area());
    // plot.scale(2);                  // error: plot is const
}
Output
Room: 8 x 5 = 40 sq m
Plot area: 200 sq m

When a member function is defined outside the class, its name gets the class prefix: Rectangle::area. The :: is the scope resolution operator.

Header and source files

In real projects each class usually has two files. The header (.h or .hpp) contains the class definition, so other files can #include it. The source file (.cpp) contains the member function bodies.

C++
// rectangle.h
#pragma once

struct Rectangle {
    double width = 0;
    double height = 0;
    double area() const;
};
C++
// rectangle.cpp
#include "rectangle.h"

double Rectangle::area() const {
    return width * height;
}
C++
// main.cpp
#include <print>
#include "rectangle.h"

int main() {
    Rectangle hall{12, 9};
    std::println("Hall area: {} sq m", hall.area());
}
Terminal
g++ -std=c++23 -Wall main.cpp rectangle.cpp -o hall
./hall
Output
Hall area: 108 sq m

#pragma once stops a header from being included twice in the same file. Your own headers use double quotes ("rectangle.h"), standard headers use angle brackets. Both .cpp files are compiled and then linked into one program. C++20 also offers modules as a modern alternative to headers; see Namespaces & Modules.

Common mistakes

  • Forgetting the semicolon after the class. struct Student { ... } must end with };. The error often appears on a later line.
  • Making everything public. Then any code can break your invariants. Keep data private when rules apply.
  • Missing const on reading functions. Without it, you cannot call the function on a const object or through a const& parameter.
  • Writing a getter and setter for every member. If a setter accepts any value, the member might as well be public. Provide operations that make sense, like deposit, instead.

Key points

  • A class bundles data members and member functions into a new type; objects are values of that type.
  • struct members are public by default, class members private; use public: and private: to control access.
  • Encapsulation keeps invariants safe by changing data only through checked member functions.
  • Mark member functions that only read with const; define functions outside the class as ClassName::function.
  • Put class definitions in headers with #pragma once and member bodies in .cpp files.

Exercise

Create a class CricketPlayer with private members name_ (string), runs_ and balls_ (both int, starting at 0). Add a public function set_name, a function add_innings(int runs, int balls) that ignores negative values, and const functions name() and strike_rate() (runs per 100 balls, 0 if no balls). Create Rohan, add innings of 45 off 30 and 82 off 61, and print his strike rate with two decimals.

Show solution
C++
#include <print>
#include <string>

class CricketPlayer {
public:
    void set_name(const std::string& name) { name_ = name; }

    void add_innings(int runs, int balls) {
        if (runs < 0 || balls < 0) {
            return;
        }
        runs_ += runs;
        balls_ += balls;
    }

    const std::string& name() const { return name_; }

    double strike_rate() const {
        if (balls_ == 0) {
            return 0.0;
        }
        return runs_ * 100.0 / balls_;
    }

private:
    std::string name_;
    int runs_ = 0;
    int balls_ = 0;
};

int main() {
    CricketPlayer rohan;
    rohan.set_name("Rohan");
    rohan.add_innings(45, 30);
    rohan.add_innings(82, 61);
    std::println("{}: strike rate {:.2f}", rohan.name(), rohan.strike_rate());
}
Output
Rohan: strike rate 139.56

Rohan has 127 runs from 91 balls, and 127 × 100 / 91 is about 139.56. Multiplying by 100.0 makes the calculation use decimals. Setting the name in a separate step is clumsy; the next lesson replaces it with a constructor.

Quick Quiz

1. What is the default access of members in a class?

2. Why mark a member function const?

3. After Student c = a; c.name = "X";, what is a.name?

4. How do you define the function area of struct Rectangle outside the struct?

Learn C++ with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call