7 Classes, Constructors, and Destructors
8 Classes, Constructors, and Destructors
Week 3, Session 1. CSS 342.
8.1 Warmup
Implement strStr() (LeetCode #28). Return the first index where needle occurs in haystack, or -1.
class Solution {
public:
int strStr(string haystack, string needle) {
int hLen = haystack.size();
int nLen = needle.size();
if (nLen > hLen) return -1;
for (int i = 0; i <= hLen - nLen; ++i) {
if (matches(haystack, needle, i)) return i;
}
return -1;
}
private:
bool matches(const string& h, const string& n, int offset) {
for (int j = 0; j < (int)n.size(); ++j) {
if (h[offset + j] != n[j]) return false;
}
return true;
}
};Notice the helper matches is written first and the public strStr calls it. Decomposition is the habit. I will say this thirty more times before chapter 20.
8.2 Learning objectives
- Write a C++ class with both default and parameterized constructors using member initializer lists.
- Implement a destructor that correctly frees dynamically allocated memory.
- Explain why the Rule of Three exists and identify which of the three functions a class needs.
- Use
this->only when necessary and critique code that overuses it. - Properly split a class into
.hand.cppfiles with include guards.
8.3 Anatomy of a class
A class has fields (state) and methods (behavior). Access modifiers control which are visible to the outside.
class Box {
public:
Box(); // default constructor
Box(int width, int height); // parameterized constructor
int area() const; // const method: cannot mutate fields
void resize(int w, int h);
private:
int width_;
int height_;
};Field names with a trailing underscore (width_) are one convention. The textbook uses width. Google style uses width_. LLVM style uses Width. Pick a convention for your class and be consistent. I use trailing underscores in this book because it makes the body of constructors easier to read (width_(width)). Outside this book I use no underscore, because life is too short to argue about style.
8.4 Constructors and member initializer lists
A constructor sets up the object. The cleanest way to initialize fields is the member initializer list — the colon notation:
Box::Box() : width_(0), height_(0) {}
Box::Box(int w, int h) : width_(w), height_(h) {}Compare to the assignment style:
Box::Box(int w, int h) {
width_ = w; // works, but inside the body
height_ = h;
}Both produce the same result for basic types. The initializer list is preferred because (a) for const and reference members it is the only way that works, (b) for complex types it avoids a default-construction-then-assignment.
8.5 The destructor
The destructor runs when an object goes out of scope (stack object) or is deleted (heap object). It is your chance to free resources. For a class that doesn’t own heap memory, the destructor can be empty or omitted entirely.
class Box {
public:
~Box() {
cout << "Box destroyed" << endl;
}
};Run this:
int main() {
Box a;
Box b;
{
Box c;
} // c destroyed here
// a, b destroyed when main returns -- in reverse order of construction
}Output:
Box destroyed <-- c
Box destroyed <-- b
Box destroyed <-- a
LIFO destruction is a guarantee. You can rely on it.
8.5.1 Destructors for classes that own heap memory
Here is where it matters:
class DynamicArray {
public:
DynamicArray(int size) : size_(size) {
data_ = new int[size];
}
~DynamicArray() {
delete[] data_; // <-- without this, leak on every destruction
}
private:
int* data_;
int size_;
};If you new anything in a constructor, the destructor must delete it. The class owns that memory and must clean up.
A class that news anything but does not implement a destructor will leak. Test with AddressSanitizer or valgrind. Always.
8.6 The this pointer
Inside a method, this is a pointer to the current object. this->field is the same as field (when there is no name collision).
class Box {
public:
void setWidth(int width) {
this->width_ = width; // disambiguates the parameter from the field
// or: width_ = width; // also fine because of the underscore
}
};Students massively overuse this->. Writing this->x_ = x_; everywhere is noise. Use this-> only when there is an actual name collision (parameter shadows field), or when you specifically want to make the call explicit (rare). The compiler does not need it. Your reader does not need it. Drop it.
8.7 The .h / .cpp split
// Box.h
#ifndef BOX_H
#define BOX_H
class Box {
public:
Box();
Box(int width, int height);
int area() const;
private:
int width_;
int height_;
};
#endif// Box.cpp
#include "Box.h"
Box::Box() : width_(0), height_(0) {}
Box::Box(int w, int h) : width_(w), height_(h) {}
int Box::area() const { return width_ * height_; }Notice that method definitions in Box.cpp are prefixed with Box::. That :: is the scope resolution operator — it says “this area belongs to the Box class.”
const after the parameter list (int area() const) is a promise: this method does not modify the object. The compiler enforces it. Get in the habit of marking accessors const — it documents intent and lets you call them on const Box& objects.
8.8 The Rule of Three — preview
Here is a sentence you will hear me say a hundred times: if your class needs a destructor, it almost certainly also needs a copy constructor and a copy assignment operator.
That is the Rule of Three. We will spend Chapter 7 on it. For now, the intuition:
- If your class manages heap memory, the destructor frees it.
- But what happens when someone copies an instance of your class? The default copy duplicates the pointer, not the data. Two objects now share one heap region.
- When the first object dies, it frees the memory. The second object now holds a dangling pointer.
- Crash, on the second destruction.
Building a class that needs all three is the centerpiece of weeks 3 and 4.
8.9 A complete example
// Box.h
#pragma once
class Box {
public:
Box();
Box(int width, int height);
int area() const;
int width() const;
int height() const;
void resize(int width, int height);
private:
int width_;
int height_;
};// Box.cpp
#include "Box.h"
Box::Box() : width_(1), height_(1) {}
Box::Box(int w, int h) : width_(w), height_(h) {}
int Box::area() const { return width_ * height_; }
int Box::width() const { return width_; }
int Box::height() const { return height_; }
void Box::resize(int w, int h) {
width_ = w;
height_ = h;
}// main.cpp
#include "Box.h"
#include <iostream>
using namespace std;
int main() {
Box small; // default: 1x1
Box big(10, 20);
cout << small.area() << endl; // 1
cout << big.area() << endl; // 200
big.resize(5, 5);
cout << big.area() << endl; // 25
}Compile:
g++ -Wall -std=c++17 main.cpp Box.cpp -o boxes && ./boxes8.10 Try it
Implement a Fraction class with a numerator, a denominator, and an add method that takes another Fraction and returns a new one. Skeleton:
class Fraction {
public:
Fraction(int num, int den);
Fraction add(const Fraction& other) const;
void print() const; // prints "n/d"
private:
int num_;
int den_;
};Hint: add should not simplify the result. We will add simplification (and operator+) in the next chapter.
8.11 Self-check
new in its constructor but has no destructor. What happens?width_ and height_ in a constructor?const and references), avoid default-then-assign, and are idiomatic.
8.12 Challenges
These are not LeetCode this time — they are class-design exercises.
- Counter: a class with a single
int count_field,increment()anddecrement()methods, andvalue(). Add a destructor that prints"Counter dying at value X". - DynamicArray: a class that wraps
int* data_andint size_. Constructor takes a size and allocates. Destructor frees. Addget(i)andset(i, v). Compile with AddressSanitizer and confirm no leak. - Box with file split: implement the full example above and verify it compiles cleanly with
-Wall -Wextra.
8.13 Where this fits
You can now build a class that owns memory. The next chapter teaches you to make it composable with the rest of C++ — adding operators so Box can be compared, printed, and added.
| You are here | Coming up |
|---|---|
Constructors, destructors, member init lists, .h/.cpp split |
Chapter 6: operator overloading and friend |