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
- Why Classes?
- Anatomy of a Class
- Access Specifiers
- Constructors
- Destructors
- Member Functions (Methods)
- Getters and Setters
- const Member Functions
- static Members
- Operator Overloading
- Separating Interface from Implementation
- Inheritance Basics
- Putting It All Together
- 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 hereOutput:
[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
}11. Separating Interface from Implementation
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);
};
#endifrectangle.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:
| Term | What it is |
|---|---|
| Class | Blueprint that defines data members and member functions |
| Object | A concrete instance created from a class |
| Encapsulation | Bundling data and functions; hiding internals with private |
| Constructor | Special function that initializes an object (same name as class, no return type) |
| Destructor | Runs when the object is destroyed; name is ~ClassName() |
| Access specifiers | public, private, protected |
| Getter / Setter | Public functions that safely read or modify private data |
const method | Guarantees the function won’t modify the object |
static member | Shared by all objects of the class; called with ClassName:: |
Scope resolution (::) | Links an out-of-class definition back to its class |
| Inheritance | Derived class gains members of a base class using : public Base |
| Polymorphism | Base pointer calls the correct derived function via virtual |
| Operator overloading | Redefining operators (+, <<, …) for custom types |
Advice for COMP 191 / 118 students. When designing a class, ask three questions:
- What data does this thing hold? → data members
- What can it do? → member functions
- 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).
