Introduction to C++ Classes

COMP 191 / COMP 118 — Introductory Programming

By this point in the course you have written programs using variables, loops, functions, and arrays. All of that code is procedural: you tell the computer what to do, step by step. C++ also supports a second paradigm called Object-Oriented Programming (OOP), in which you model real-world things as objects that bundle data and behavior together. The blueprint for an object is called a class.

This tutorial walks through everything you need to know about C++ classes in a logical order, with fully-runnable examples at every step.

Contents

  1. Why Classes?
  2. Anatomy of a Class
  3. Access Specifiers
  4. Constructors
  5. Destructors
  6. Member Functions (Methods)
  7. Getters and Setters
  8. const Member Functions
  9. static Members
  10. Operator Overloading
  11. Separating Interface from Implementation
  12. Inheritance Basics
  13. Putting It All Together
  14. Summary & Key Terms

1. Why Classes?

Imagine you need to track information about a student: a name, an ID number, and a GPA. Without classes you might use separate variables — or parallel arrays if you have many students:

// Procedural approach — gets messy fast
string name1 = "Alice";
int    id1   = 1001;
double gpa1  = 3.85;

string name2 = "Bob";
int    id2   = 1002;
double gpa2  = 2.70;

With classes you define a single Student type once and create as many objects as you need, each carrying its own data:

// Object-oriented approach
Student alice("Alice", 1001, 3.85);
Student bob("Bob",   1002, 2.70);

alice.printInfo();
bob.printInfo();

Classes bring three big benefits:

  • Encapsulation — data and the functions that operate on it live together.
  • Abstraction — users of your class interact through a clean interface and don’t need to know the internals.
  • Reusability — write the class once, use it anywhere (and build on it via inheritance).

2. Anatomy of a Class

A class definition uses the class keyword, followed by a name and a body in curly braces. The body lists data members (variables that belong to each object) and member functions (functions that operate on those variables). A semicolon closes the definition.

#include <iostream>
#include <string>
using namespace std;

class Rectangle {
public:                        // accessible from outside
    double width;
    double height;

    double area() {
        return width * height;
    }

    double perimeter() {
        return 2 * (width + height);
    }
};

int main() {
    Rectangle r;          // create an object
    r.width  = 5.0;
    r.height = 3.0;

    cout << "Area:      " << r.area()      << "\n";
    cout << "Perimeter: " << r.perimeter() << "\n";

    return 0;
}

Output:

Area:      15
Perimeter: 16

Terminology. An object is an instance of a class — just as 5 is an instance of int. Creating an object is called instantiation. You access members with the dot operator (.).


3. Access Specifiers

C++ uses three keywords to control who can see each member:

  • public — anyone can access it.
  • private — only code inside the class can access it (the default if you write nothing).
  • protected — accessible inside the class and by derived (child) classes.

Good practice is to make data members private and expose only what’s needed through public functions — that’s encapsulation in action.

class BankAccount {
private:
    double balance;        // hidden from outside

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

    void withdraw(double amount) {
        if (amount > 0 && amount <= balance)
            balance -= amount;
        else
            cout << "Insufficient funds.\n";
    }

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

int main() {
    BankAccount acct;
    acct.deposit(500.0);
    acct.withdraw(120.0);
    cout << "Balance: $" << acct.getBalance() << "\n";

    // acct.balance = 9999;  // ERROR — balance is private
    return 0;
}

Output:

Balance: $380

Directly setting acct.balance = 9999; causes a compile error because balance is private. This protects the object from invalid states.


4. Constructors

A constructor is a special function that runs automatically when an object is created. It has the same name as the class and no return type. Its job is to initialize the object’s data members to sensible starting values.

4a. Default Constructor

Takes no parameters; sets safe defaults.

class Counter {
private:
    int count;

public:
    Counter() {           // default constructor
        count = 0;
    }

    void increment() { count++; }
    void decrement() { count--; }
    int  getCount() const { return count; }
};

int main() {
    Counter c;            // count is 0 automatically
    c.increment();
    c.increment();
    c.increment();
    c.decrement();
    cout << "Count: " << c.getCount() << "\n";  // Count: 2
}

4b. Parameterized Constructor

Accepts arguments so the caller can supply initial values at creation time.

class Student {
private:
    string name;
    int    id;
    double gpa;

public:
    Student(string n, int i, double g) {   // parameterized
        name = n;
        id   = i;
        gpa  = g;
    }

    void printInfo() const {
        cout << "Name: " << name
             << "  ID: " << id
             << "  GPA: " << gpa << "\n";
    }
};

int main() {
    Student alice("Alice", 1001, 3.85);
    Student bob  ("Bob",   1002, 2.70);

    alice.printInfo();
    bob.printInfo();
}

Output:

Name: Alice  ID: 1001  GPA: 3.85
Name: Bob    ID: 1002  GPA: 2.7

4c. Constructor Initializer List

A cleaner, often faster way to initialize members is the initializer list — a colon followed by member assignments before the opening brace:

class Point {
public:
    double x, y;

    // Initializer list form
    Point(double px, double py) : x(px), y(py) {}

    void print() const {
        cout << "(" << x << ", " << y << ")\n";
    }
};

int main() {
    Point p(3.0, 4.0);
    p.print();             // (3, 4)
}

4d. Constructor Overloading

You can have multiple constructors — one for each set of arguments you want to support:

class Box {
private:
    double length, width, height;

public:
    Box() : length(1), width(1), height(1) {}           // unit cube
    Box(double s) : length(s), width(s), height(s) {}       // cube with side s
    Box(double l, double w, double h)
        : length(l), width(w), height(h) {}                 // full box

    double volume() const { return length * width * height; }
};

int main() {
    Box unit;              cout << unit.volume() << "\n";   // 1
    Box cube(3.0);        cout << cube.volume() << "\n";   // 27
    Box box(2, 3, 4);    cout << box.volume()  << "\n";   // 24
}

5. Destructors

A destructor runs automatically when an object goes out of scope or is deleted. It has the class name preceded by a tilde (~) and takes no parameters. For simple classes you rarely write one — the compiler generates a default. For classes that allocate resources (dynamic memory, open files, network connections) the destructor is where you release them.

#include <iostream>
using namespace std;

class Logger {
private:
    string tag;

public:
    Logger(string t) : tag(t) {
        cout << "[" << tag << "] created\n";
    }

    ~Logger() {                           // destructor
        cout << "[" << tag << "] destroyed\n";
    }
};

int main() {
    Logger outer("Outer");
    {
        Logger inner("Inner");
        cout << "  -- inside block --\n";
    }                               // inner destroyed here
    cout << "  -- back in main --\n";
}                                   // outer destroyed here

Output:

[Outer] created
[Inner] created
  -- inside block --
[Inner] destroyed
  -- back in main --
[Outer] destroyed

Destructors are called in reverse order of construction — last in, first out. This is why C++ resource management is so predictable.


6. Member Functions (Methods)

Member functions operate on the object’s own data through the implicit this pointer. You can define them either inside or outside the class body.

6a. Defined Inside the Class

class Circle {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    double area() const {               // defined inside
        return 3.14159 * radius * radius;
    }

    double circumference() const {
        return 2 * 3.14159 * radius;
    }
};

6b. Defined Outside the Class (Scope Resolution Operator)

For larger classes, definitions are placed outside using the scope resolution operator ::. This is the standard pattern when splitting classes across header and source files.

class Circle {
private:
    double radius;

public:
    Circle(double r);           // declaration only
    double area() const;
    double circumference() const;
};

// Definitions outside — note ClassName::
Circle::Circle(double r) : radius(r) {}

double Circle::area() const {
    return 3.14159 * radius * radius;
}

double Circle::circumference() const {
    return 2 * 3.14159 * radius;
}

int main() {
    Circle c(7.0);
    cout << "Area: "          << c.area()          << "\n";
    cout << "Circumference: " << c.circumference() << "\n";
}

7. Getters and Setters

When data members are private, you provide getter functions to read them and setter functions to modify them. Setters can validate input before accepting it.

class Temperature {
private:
    double celsius;

public:
    Temperature(double c = 20.0) : celsius(c) {}

    // Getter
    double getCelsius() const { return celsius; }

    // Computed getters
    double getFahrenheit() const { return celsius * 9.0/5.0 + 32; }
    double getKelvin() const     { return celsius + 273.15; }

    // Setter with validation
    void setCelsius(double c) {
        if (c < -273.15)
            cout << "Error: below absolute zero!\n";
        else
            celsius = c;
    }
};

int main() {
    Temperature t(100.0);
    cout << t.getCelsius()    << " C\n";   // 100 C
    cout << t.getFahrenheit() << " F\n";   // 212 F
    cout << t.getKelvin()     << " K\n";   // 373.15 K

    t.setCelsius(-300);                     // rejected
    t.setCelsius(25);
    cout << t.getFahrenheit() << " F\n";   // 77 F
}

8. const Member Functions

Placing const after a member function’s parameter list tells the compiler “this function will not modify the object”. It is good practice to mark every function that only reads data as const. const objects can only call const member functions.

class Vector2D {
public:
    double x, y;

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

    double magnitude() const {         // read-only — OK on const object
        return sqrt(x*x + y*y);
    }

    void scale(double factor) {         // modifies — NOT ok on const object
        x *= factor;
        y *= factor;
    }
};

int main() {
    const Vector2D v(3.0, 4.0);

    cout << v.magnitude() << "\n";   // OK — 5
    // v.scale(2.0);                 // COMPILE ERROR — v is const
}

9. static Members

A static data member is shared by all objects of the class — there is only one copy regardless of how many objects exist. A static member function can be called without an object; it can only access static members.

class Car {
private:
    string make;
    static int totalCars;        // shared across ALL Car objects

public:
    Car(string m) : make(m) {
        totalCars++;             // every new Car bumps the count
    }

    ~Car() {
        totalCars--;
    }

    static int getTotal() {    // static function — no object needed
        return totalCars;
    }

    string getMake() const { return make; }
};

// Static member must be defined outside the class
int Car::totalCars = 0;

int main() {
    cout << Car::getTotal() << "\n";   // 0
    {
        Car a("Toyota"), b("Honda"), c("Ford");
        cout << Car::getTotal() << "\n"; // 3
    }                                     // a, b, c destroyed
    cout << Car::getTotal() << "\n";   // 0
}

10. Operator Overloading

C++ lets you redefine how operators (+, -, <<, etc.) behave for your class. This makes objects feel natural to use.

#include <iostream>
using namespace std;

class Fraction {
private:
    int num, den;

    int gcd(int a, int b) const { return b == 0 ? a : gcd(b, a % b); }

    void reduce() {
        int g = gcd(abs(num), abs(den));
        num /= g;  den /= g;
        if (den < 0) { num = -num; den = -den; }
    }

public:
    Fraction(int n = 0, int d = 1) : num(n), den(d) { reduce(); }

    Fraction operator+(const Fraction& rhs) const {
        return Fraction(num * rhs.den + rhs.num * den, den * rhs.den);
    }

    Fraction operator*(const Fraction& rhs) const {
        return Fraction(num * rhs.num, den * rhs.den);
    }

    bool operator==(const Fraction& rhs) const {
        return num == rhs.num && den == rhs.den;
    }

    // Output stream operator (friend so it can access private members)
    friend ostream& operator<<(ostream& os, const Fraction& f) {
        os << f.num;
        if (f.den != 1) os << "/" << f.den;
        return os;
    }
};

int main() {
    Fraction a(1, 2), b(1, 3);

    cout << a << " + " << b << " = " << (a + b) << "\n";  // 1/2 + 1/3 = 5/6
    cout << a << " * " << b << " = " << (a * b) << "\n";  // 1/2 * 1/3 = 1/6

    Fraction c(2, 4);   // reduces to 1/2
    cout << (a == c ? "Equal" : "Not equal") << "\n";   // Equal
}

Real-world C++ projects split each class into two files:

  • Header file (.h) — the class declaration (what it is).
  • Source file (.cpp) — the function definitions (how it works).

This keeps things organized and allows multiple source files to share the same class without duplicating code.

rectangle.h

// rectangle.h
#ifndef RECTANGLE_H       // include guard — prevents double inclusion
#define RECTANGLE_H

class Rectangle {
private:
    double width, height;

public:
    Rectangle(double w, double h);
    double area() const;
    double perimeter() const;
    void   scale(double factor);
};

#endif

rectangle.cpp

// rectangle.cpp
#include "rectangle.h"

Rectangle::Rectangle(double w, double h) : width(w), height(h) {}

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

void Rectangle::scale(double factor) {
    width  *= factor;
    height *= factor;
}

main.cpp

// main.cpp
#include <iostream>
#include "rectangle.h"
using namespace std;

int main() {
    Rectangle r(4.0, 6.0);
    cout << "Area: " << r.area() << "\n";      // 24
    r.scale(2.0);
    cout << "Area: " << r.area() << "\n";      // 96
    return 0;
}

Compile with: g++ main.cpp rectangle.cpp -o myprogram


12. Inheritance Basics

Inheritance lets one class (the derived or child class) acquire the members of another (the base or parent class). Use class Child : public Parent { ... };. The child can then add new members or override existing ones.

#include <iostream>
#include <string>
using namespace std;

// ─── Base class ───────────────────────────────────────────
class Animal {
protected:
    string name;
    int    age;

public:
    Animal(string n, int a) : name(n), age(a) {}

    void breathe() const {
        cout << name << " breathes.\n";
    }

    virtual void speak() const {       // virtual enables overriding
        cout << name << " makes a sound.\n";
    }
};

// ─── Derived class ────────────────────────────────────────
class Dog : public Animal {
private:
    string breed;

public:
    Dog(string n, int a, string b)
        : Animal(n, a), breed(b) {}

    void speak() const override {      // override the base version
        cout << name << " barks!\n";
    }

    void fetch() const {               // new behavior only in Dog
        cout << name << " fetches the ball.\n";
    }
};

class Cat : public Animal {
public:
    Cat(string n, int a) : Animal(n, a) {}

    void speak() const override {
        cout << name << " meows.\n";
    }
};

int main() {
    Dog rex("Rex", 3, "Labrador");
    Cat whiskers("Whiskers", 5);

    rex.breathe();   // inherited from Animal
    rex.speak();     // Dog's version
    rex.fetch();     // Dog only

    whiskers.breathe();
    whiskers.speak();  // Cat's version

    // Polymorphism: base pointer calls correct derived version
    Animal* a = &rex;
    a->speak();     // still calls Dog::speak() — that's polymorphism!
}

Output:

Rex breathes.
Rex barks!
Rex fetches the ball.
Whiskers breathes.
Whiskers meows.
Rex barks!

The virtual keyword enables polymorphism: when you call a virtual function through a base-class pointer or reference, C++ automatically calls the correct derived version at runtime. Always add override in the derived class — it makes your intent clear and the compiler will catch typos in the function signature.


13. Putting It All Together — A Complete Example

Let’s build a small Library System that uses multiple classes together. It demonstrates constructors, encapsulation, getters/setters, and a simple collection.

#include <iostream>
#include <string>
#include <vector>
using namespace std;

// ─── Book ─────────────────────────────────────────────────
class Book {
private:
    string title;
    string author;
    bool   checkedOut;

public:
    Book(string t, string a)
        : title(t), author(a), checkedOut(false) {}

    string getTitle()  const { return title; }
    string getAuthor() const { return author; }
    bool   isOut()     const { return checkedOut; }

    bool checkOut() {
        if (checkedOut) { cout << "Already checked out.\n"; return false; }
        checkedOut = true;
        return true;
    }

    void returnBook() { checkedOut = false; }

    void print() const {
        cout << "  \"" << title << "\" by " << author
             << (checkedOut ? "  [OUT]" : "  [IN]") << "\n";
    }
};

// ─── Library ──────────────────────────────────────────────
class Library {
private:
    string       name;
    vector<Book> collection;

public:
    Library(string n) : name(n) {}

    void addBook(const Book& b) {
        collection.push_back(b);
    }

    void listAll() const {
        cout << "\n=== " << name << " ===\n";
        for (const auto& b : collection)
            b.print();
    }

    bool checkOut(const string& title) {
        for (auto& b : collection)
            if (b.getTitle() == title)
                return b.checkOut();
        cout << "Book not found: " << title << "\n";
        return false;
    }

    void returnBook(const string& title) {
        for (auto& b : collection)
            if (b.getTitle() == title) { b.returnBook(); return; }
        cout << "Book not found.\n";
    }

    int available() const {
        int n = 0;
        for (const auto& b : collection)
            if (!b.isOut()) n++;
        return n;
    }
};

// ─── main ─────────────────────────────────────────────────
int main() {
    Library lib("City Library");

    lib.addBook(Book("The C++ Programming Language", "Stroustrup"));
    lib.addBook(Book("Clean Code",                   "Martin"));
    lib.addBook(Book("Design Patterns",               "GoF"));

    lib.listAll();

    lib.checkOut("Clean Code");
    lib.checkOut("Clean Code");     // attempt again
    lib.checkOut("Dune");           // not in library

    lib.listAll();

    cout << "\nBooks available: " << lib.available() << "\n";

    lib.returnBook("Clean Code");
    cout << "Books available: " << lib.available() << "\n";

    return 0;
}

Output:

=== City Library ===
  "The C++ Programming Language" by Stroustrup  [IN]
  "Clean Code" by Martin  [IN]
  "Design Patterns" by GoF  [IN]
Already checked out.
Book not found: Dune

=== City Library ===
  "The C++ Programming Language" by Stroustrup  [IN]
  "Clean Code" by Martin  [OUT]
  "Design Patterns" by GoF  [IN]

Books available: 2
Books available: 3

14. Summary & Key Terms

You’ve now seen all the essential building blocks of C++ classes. Here’s a quick reference:

TermWhat it is
ClassBlueprint that defines data members and member functions
ObjectA concrete instance created from a class
EncapsulationBundling data and functions; hiding internals with private
ConstructorSpecial function that initializes an object (same name as class, no return type)
DestructorRuns when the object is destroyed; name is ~ClassName()
Access specifierspublic, private, protected
Getter / SetterPublic functions that safely read or modify private data
const methodGuarantees the function won’t modify the object
static memberShared by all objects of the class; called with ClassName::
Scope resolution (::)Links an out-of-class definition back to its class
InheritanceDerived class gains members of a base class using : public Base
PolymorphismBase pointer calls the correct derived function via virtual
Operator overloadingRedefining operators (+, <<, …) for custom types

Advice for COMP 191 / 118 students. When designing a class, ask three questions:

  1. What data does this thing hold? → data members
  2. What can it do? → member functions
  3. What should the outside world be allowed to touch? → access specifiers

Start simple, test each class individually, then combine them.


End of tutorial — COMP 191 / COMP 118 Introduction to C++ Classes.
Content prepared for course use. Code examples compiled and tested with g++ (C++17).

Scroll to Top