FirstHack Learn
Log in Sign up free

Classes and Objects

10 min read · 30 views

Classes and Objects

This is the feature C++ was created for. Everything else in the language is refinement; classes are the reason it exists at all.

The idea is simple. Some data belongs together, and some operations only make sense on that data. A class puts both in one place and lets you control who may touch what.

Starting from a struct

A struct groups related variables under one name. C++ inherits it from C and extends it.

C++
#include <iostream>
#include <string>

struct Point {
    double x;
    double y;
};

int main() {
    Point a;
    a.x = 3.0;
    a.y = 4.0;

    Point b = {1.0, 2.0};

    std::cout << "a is at (" << a.x << ", " << a.y << ")\n";
    std::cout << "b is at (" << b.x << ", " << b.y << ")\n";

    double dx = a.x - b.x;
    double dy = a.y - b.y;
    std::cout << "Difference: (" << dx << ", " << dy << ")\n";

    return 0;
}

The dot operator reaches a member. Point is now a type like any other, so you can have a std::vector<Point> or a function that takes a Point.

This works, and for a plain bundle of coordinates it is exactly right. The trouble starts when the data has rules. A BankAccount must never have a negative balance. A Date must never have month 13. With a plain struct, every piece of code that touches the data is responsible for remembering the rules, and eventually one of them forgets.

struct versus class

In C++ the two keywords are almost the same thing. The only difference is the default access level: members of a struct are public unless you say otherwise, and members of a class are private unless you say otherwise.

That is genuinely the whole language difference. The convention that grew around it is useful, though: use struct for a passive bundle of data with no rules to enforce, and class when the type has behaviour and invariants to protect.

Access specifiers and encapsulation

public members can be used by anyone. private members can be used only by the class's own member functions.

Making data private is called encapsulation, and it is worth being clear about why it helps, because "hiding data" sounds like bureaucracy. The real benefit is that it shrinks the amount of code you have to read when something goes wrong. If balance is public and it holds a wrong value, the culprit is anywhere in the program. If balance is private, the culprit is inside the class, which might be forty lines. That is the entire argument, and it is a strong one.

Constructors

A constructor is a member function with the same name as the class and no return type. It runs automatically when an object is created, which is what makes it useful: there is no way to obtain an object without it having run, so it is the right place to establish the rules the class depends on.

C++
#include <iostream>
#include <string>

class Student {
private:
    std::string name;
    int rollNumber;
    double cgpa;

public:
    Student(const std::string& n, int roll, double gpa) {
        name = n;
        rollNumber = roll;
        cgpa = gpa;
        if (cgpa < 0.0) cgpa = 0.0;
        if (cgpa > 10.0) cgpa = 10.0;
    }

    Student(const std::string& n, int roll)
        : name(n), rollNumber(roll), cgpa(0.0) {
    }

    std::string getName() const { return name; }
    int getRoll() const { return rollNumber; }
    double getCgpa() const { return cgpa; }

    void setCgpa(double gpa) {
        if (gpa < 0.0 || gpa > 10.0) {
            std::cout << "Rejected invalid CGPA: " << gpa << "\n";
            return;
        }
        cgpa = gpa;
    }

    void print() const {
        std::cout << rollNumber << "  " << name << "  CGPA " << cgpa << "\n";
    }
};

int main() {
    Student a("Ananya Rao", 101, 8.6);
    Student b("Vikram Singh", 102);

    a.print();
    b.print();

    b.setCgpa(7.2);
    b.print();

    b.setCgpa(15.0);
    b.print();

    std::cout << a.getName() << " has CGPA " << a.getCgpa() << "\n";
    return 0;
}

Several things are happening here that are worth naming.

There are two constructors, which is ordinary function overloading applied to constructors. One takes a CGPA, one defaults it to zero.

The second constructor uses an initialiser list, the part after the colon. It initialises members directly rather than assigning to them after they already exist. For simple types the difference is small; for members that are themselves objects it avoids a wasted default construction, and for const members it is the only way. Prefer it.

The const after getName() const promises the function does not modify the object. This lets you call it on a const Student, and it lets the compiler catch an accidental write. Mark every member function that only reads as const.

setCgpa is where encapsulation earns its keep. Because cgpa is private, the range check is the only door in. A public member variable would have no door at all.

A worked example

Here is a small class that maintains a running total, showing state that changes over time.

C++
#include <iostream>
#include <vector>
#include <string>

class Marksheet {
private:
    std::string studentName;
    std::vector<int> scores;

public:
    Marksheet(const std::string& name) : studentName(name) {}

    void addScore(int s) {
        if (s < 0 || s > 100) {
            std::cout << "Ignoring out-of-range score: " << s << "\n";
            return;
        }
        scores.push_back(s);
    }

    int count() const {
        return (int)scores.size();
    }

    int total() const {
        int t = 0;
        for (int s : scores) t += s;
        return t;
    }

    double average() const {
        if (scores.empty()) return 0.0;
        return (double)total() / scores.size();
    }

    int highest() const {
        if (scores.empty()) return 0;
        int best = scores[0];
        for (int s : scores) {
            if (s > best) best = s;
        }
        return best;
    }

    void report() const {
        std::cout << "Marksheet for " << studentName << "\n";
        std::cout << "  Subjects: " << count() << "\n";
        std::cout << "  Total:    " << total() << "\n";
        std::cout << "  Average:  " << average() << "\n";
        std::cout << "  Highest:  " << highest() << "\n";
        std::cout << "  Result:   " << (average() >= 40.0 ? "Pass" : "Fail") << "\n";
    }
};

int main() {
    Marksheet m("Ananya Rao");

    m.addScore(78);
    m.addScore(65);
    m.addScore(92);
    m.addScore(120);
    m.addScore(41);

    m.report();

    Marksheet empty("New Student");
    empty.report();

    return 0;
}

The invalid score of 120 never enters scores, and the empty marksheet reports zeroes instead of dividing by zero. Both guarantees live inside the class, so no caller can break them by mistake.

💡Ask what the object promises

Before writing members, finish this sentence: "an object of this class always guarantees that ...". For Marksheet it is "every stored score is between 0 and 100". That sentence tells you which data must be private and which checks belong in the constructor and setters. A class with no such promise is probably better off as a plain struct.

Objects in containers

Because a class is a type, a std::vector<Marksheet> works exactly as you would expect, and you can loop over it with a range-based for. Pass objects to functions as const ClassName& to avoid copying.

Common mistakes

  • Forgetting the semicolon after the closing brace of a class. }; is required. Leaving it out produces a confusing error pointing at the next line.
  • Trying to access a private member from main. The error says the member is private in this context. Add a public getter, or reconsider whether it should be private.
  • Writing a return type on the constructor. void Student(...) is not a constructor; it is an ordinary member function that happens to share the name. Constructors have no return type at all.
  • Defining a constructor with arguments and then writing Student s;. Once you define any constructor, the compiler no longer supplies the default one. Either pass arguments or define a no-argument constructor as well.
  • Forgetting const on read-only member functions. Everything works until you have a const object or a const reference parameter, and then a pile of errors appears.
  • Making everything public "for now". The rules stop being enforceable and the class becomes a struct with extra typing.

Classes are the point at which you start modelling the problem rather than the steps. Take an exercise you have already solved with loose functions and rewrite it as one class with private data. Then work through the object-oriented problems on the Practice page.

Create a free account to track what you have finished.