CS 112

Introduction to Data Structures

Week 03

Classes and Operator Overloading

Eric Araújo

Calvin University · Fall 2026

This Week

  • 1
    A class is a new type
  • 2
    Interface vs. implementation
  • 3
    public, private, const
  • 4
    Constructors and destructors
  • 5
    Test-driven development
  • 6
    Operator overloading KEY
  • 7
    Designing a class together

A Class Is a New Type

A class bundles data with the operations that make sense on it, and hides how the data is actually stored.

Built in


int    x = 7;
double d = 3.14;
      
x 7
d 3.14

One name, one box.

Yours


Pair p(7, 11);
Enemy goblin("green", 3);
      
pmyFirst7mySecond11
goblincolor"green"level3

One name, one box, built from smaller boxes side by side.

p is not a pointer and not a list. It is one variable, exactly like x, just as wide as its members put together.

Interface and Implementation

Pair.h: what exists


class Pair {
 public:
  Pair();
  Pair(Item f, Item s);
  Item getFirst() const;
  Item getSecond() const;
 private:
  Item myFirst;
  Item mySecond;
};
      

Pair.cpp: how it works


#include "Pair.h"

Pair::Pair() {
  myFirst  = 0;
  mySecond = 0;
}

Item Pair::getFirst() const {
  return myFirst;
}
      

What Pair:: Does

The header said the function exists. The .cpp has to say which class it belongs to, or you have written something else entirely.

Without it


// Pair.cpp
Item getFirst() const {
  return myFirst;  // error: not declared
}                  // error: const, no object
      

With it


// Pair.cpp
Item Pair::getFirst() const {
  return myFirst;  // this object's member
}                  // const: will not change it
      
:: is the scope resolution operator. Read Pair::getFirst right to left: "getFirst, the one that belongs to Pair". It is the same thing you read in compiler messages, where std::string is "string, the one that belongs to std".

public and private

  • public: anyone may use it. This is your promise to the outside world.
  • private: only the class's own methods may touch it. Default for data.

Pair.h


class Pair {
 public:
  Pair(Item f, Item s);
  Item getFirst() const;
 private:
  Item myFirst;
  Item mySecond;
};
      

main.cpp


#include "Pair.h"
...
int main() {
  Pair p(7, 11);
  ...
  p.getFirst();  // it is public
  p.myFirst;     // it is private
                 // compile error
}
      
Talk to your neighbor · TTYN
You decide to store the two items in an array instead of two separate members. Whose code has to be rewritten?

// Pair.h, before          // Pair.h, after
 private:                  private:
  Item myFirst;             Item myItems[2];
  Item mySecond;
    
  • A. Pair.h and Pair.cpp only
  • B. Pair.h, Pair.cpp, and every program that uses Pair
  • C. Pair.cpp only: the header does not mention storage
  • D. Every program that uses Pair, but not Pair itself
✓ Answer
A. The data is private, so no program outside Pair was ever allowed to name myFirst. Nothing out there can break, because nothing out there was reaching in. You rewrite the private half of the header and the methods that use it; the public half, the part you promised, does not move. That is what private buys you.

const Methods


Item getFirst() const;   // promises not to change it
void setFirst(Item v);   // may change it
  
  • The const goes after the parameter list
  • Inside a const method, assigning to a member is a compile error
  • Only const methods may be called on a const object

Constructors


class Pair {
 public:
  Pair();                // same name, no return type
  Pair(Item f, Item s);  // a second one, other parameters
  ...
 private:
  Item myFirst;          // a constructor must leave these
  Item mySecond;         // two in a valid state, never junk
};

Pair a;                  // creating it runs Pair()
Pair b(7, 11);           // this one runs Pair(Item, Item)
  

Destructors


class Pair {
 public:
  ~Pair();      // no args, no return
};
  

It runs automatically when the object dies:

  • a local object reaches the end of its scope
  • a delete is called on it
  • the object it lives inside is destroyed
For Pair you don't need to write one. Next week, when objects start owning memory from new, you will, and forgetting it leaks.

Write the Tests First


TEST_CASE("Pair stores what we give it") {
    Pair p(7, 11);
    REQUIRE(p.getFirst()  == 7);
    REQUIRE(p.getSecond() == 11);
}

TEST_CASE("default Pair starts at zero") {
    Pair p;
    REQUIRE(p.getFirst() == 0);
}
  
Writing the test first forces the question what should this class do? before how do I build it? The tests become the specification, and they stay useful when you change the implementation.

Demo

$ make test Pair.h:12: error: 'myFirst' is private within this context $

Building a class from its tests.

(private means private, even to the person who typed it)

Why Overload an Operator?

What you want to write


Pair p(7, 11);
cout << p << endl;
      
and the compiler says

error: no match for 'operator<<' (operand types are 'std::ostream' and 'Pair')

cout was written years before your class existed. Nobody ever told it what a Pair looks like.

What you write instead


cout << p.getFirst() << ", "
     << p.getSecond() << endl;
      

Every time. In every program. In every loop that prints a hundred of them.

Teach cout once what a Pair looks like, and cout << p works everywhere after that. That is the whole of operator overloading.

Overloading <<


void operator<<(ostream &out, const Pair &p) {
    out << "(" << p.getFirst() << ", "
        << p.getSecond() << ")";
}

cout << p;              // ✓ works
  
Not a method, a free function. The left operand is the ostream, not your object.

Overloading <<


ostream& operator<<(ostream &out, const Pair &p) {
    out << "(" << p.getFirst() << ", "
        << p.getSecond() << ")";
    return out;              // hand the stream back
}

cout << p << endl;       // ✓ now this works
  
cout << p << endl is really (cout << p) << endl. If the first call returns void, there's no stream left for endl. Returning ostream& is what makes chaining work.
Talk to your neighbor · TTYN
Why must operator<< return ostream& rather than ostream?
  • A. To avoid copying the stream, streams cannot be copied
  • B. So that endl can be used
  • C. Because cout is global
  • D. It makes no difference
✓ Answer
A. Returning by value would try to copy the stream object, which C++ forbids: a stream is a unique handle on a device. The reference hands back the same stream, which is also what allows chaining.

Design One Together: Enemy

What data?

  • color
  • speed
  • hit points

What operations?

  • take damage
  • is it still alive?
  • print it
With your neighbor: which members are private? Which methods are const? What is the first test you would write?

Week 03 Recap

  • A class is a new type: data plus the operations that belong with it
  • .h declares the interface, .cpp holds the implementation
  • private data buys you the freedom to change how it's stored
  • Constructors run on creation, destructors on death: both automatically
  • Write the tests first; they are the specification
  • Return ostream& from operator<< so output can chain

This deck stands on earlier CS112 materials by
Joel Adams and Victor Norman · adapted and extended by Eric Araújo