Command Palette

Search for a command to run...

MTH 4300

Lecture 9

Structs and Classes

A struct is a bundle of data. A class extends that idea: it bundles data and the functions that operate on it, and controls who can access what.

The only technical difference between struct and class in C++ is the default access level: members of a struct are public by default; members of a class are private by default.

struct vs class
struct Point {
    double x; // public by default
    double y;
};

class Point {
    double x; // private by default
    double y;
};

In practice: use struct for plain data bundles with no behavior, class for anything with encapsulated state and methods.

Access Specifiers

Access specifiers control which code can see a member:

  • public — accessible from anywhere
  • private — accessible only from within the class itself
  • protected — accessible from the class and its subclasses (covered in Lecture 13)
Access specifiers
class Counter {
public:
    void increment() { count_++; }
    int value() const { return count_; }

private:
    int count_ = 0; // only Counter's own methods can touch this
};

Hiding state behind private is encapsulation — the class controls its own invariants. Callers can only affect state through the public interface.

Member Variables and Member Functions

Member functions are declared inside the class body and defined outside using the scope resolution operator :::

Declaration and definition
class Counter {
public:
    void increment();
    int value() const;
    void reset();

private:
    int count_ = 0;
};

void Counter::increment()   { count_++; }
int  Counter::value() const { return count_; }
void Counter::reset()       { count_ = 0; }

Counter::increment tells the compiler this is Counter's increment, not a free function.

Constructors and Member Initializer Lists

A constructor runs automatically when an object is created. It has no return type and the same name as the class.

Default and parameterized constructors
class Counter {
public:
    Counter();           // default constructor
    Counter(int start);  // parameterized constructor

private:
    int count_;
};

Counter::Counter()          : count_(0)     {}
Counter::Counter(int start) : count_(start) {}

The : count_(0) syntax is a member initializer list — it initializes members before the constructor body runs. Prefer it over assigning inside the body.

Members are initialized in declaration order, not the order they appear in the initializer list. If initializers depend on each other, write the list in the same order as the declarations.

If you define any constructor, the compiler no longer generates a default constructor. Declare one explicitly if you still need it.

Destructors

A destructor runs automatically when an object goes out of scope or is deleted. Its job is to release any resources the object owns.

Destructor
class Counter {
public:
    ~Counter() { /* release resources */ }
};

The destructor has no return type, no parameters, and is prefixed with ~. A class with no heap resources usually needs no explicit destructor — the compiler provides one that does nothing.

const Member Functions

A method declared const promises not to modify the object. Mark any method that only reads state as const:

const methods
class Counter {
public:
    int  value() const { return count_; } // callable on const Counter
    void increment()   { count_++; }      // not callable on const Counter

private:
    int count_ = 0;
};

const Counter c;
c.value();     // ok
c.increment(); // compile error

The this Pointer

Inside any non-static member function, this is a pointer to the object the method was called on. You rarely need it explicitly, but it's useful when a parameter name shadows a member:

this pointer
class Counter {
public:
    void set(int count) {
        this->count_ = count; // this->count_ is the member; count is the parameter
    }
private:
    int count_ = 0;
};

Building the String Class

From here through Lecture 13, we will build a String class step by step. A String owns a heap-allocated character buffer and tracks its length.

Two helpers from <cstring>:

  • std::strlen(s) — length of a null-terminated C-string (not counting '\0')
  • std::strcpy(dst, src) — copies a C-string from src into dst
String — declaration (Lecture 9)
#include <cstddef>
#include <stdexcept>

class String {
public:
    String();                       // empty string
    explicit String(const char* s); // construct from C-string literal
    ~String();                      // free the buffer

    size_t size()  const;
    bool   empty() const;
    char   at(size_t i) const;      // bounds-checked read
    const char* c_str() const;      // raw read-only pointer

private:
    size_t len_;   // declared first — initializer list order matters
    char*  data_;
};
String — definitions (Lecture 9)
#include <cstring>

String::String()
    : len_(0), data_(new char[1]) {
    data_[0] = '\0';
}

String::String(const char* s)
    : len_(std::strlen(s)), data_(new char[len_ + 1]) {
    std::strcpy(data_, s);
}

String::~String() {
    delete[] data_;
}

size_t String::size()  const { return len_; }
bool   String::empty() const { return len_ == 0; }

char String::at(size_t i) const {
    if (i >= len_) throw std::out_of_range("String::at: index out of range");
    return data_[i];
}

const char* String::c_str() const { return data_; }

explicit on the constructor prevents the compiler from silently converting a const char* into a String. Without it, a function accepting a String would also accept a raw literal with no visible conversion at the call site.

Exercises

Q1.

Without running the code, predict what this program prints.

#include <iostream>

class Widget {
public:
    Widget(int id) : id_(id) {
        std::cout << "Widget(" << id_ << ")\n";
    }
    ~Widget() {
        std::cout << "~Widget(" << id_ << ")\n";
    }
private:
    int id_;
};

int main() {
    Widget a(1);
    Widget b(2);
    {
        Widget c(3);
        Widget d(4);
    }
    Widget e(5);
    return 0;
}

Q2.

Without running the code, predict what this program prints.

#include <iostream>

class Box {
public:
    Box(int v) : val_(v) {}

    int  val() const  { return val_; }
    void double_val() { val_ *= 2; }

private:
    int val_;
};

int main() {
    Box b(5);
    std::cout << b.val() << "\n";

    b.double_val();
    std::cout << b.val() << "\n";

    const Box cb(3);
    std::cout << cb.val() << "\n";

    return 0;
}

Q3.

Each snippet has a bug. Identify what's wrong.

// Snippet A
class Foo {
public:
    int x;
private:
    int y;
};
Foo f;
f.y = 10;

// Snippet B
class Bar {
public:
    Bar(int v) : val_(v) {}
    int val() { return val_; } // not const
private:
    int val_;
};
const Bar b(5);
std::cout << b.val() << "\n";

// Snippet C
class Baz {
public:
    Baz(int v) : val_(v) {}
private:
    int val_;
};
Baz obj; // no-argument construction

// Snippet D
class Leaky {
public:
    Leaky() : data_(new int[100]) {}
private:
    int* data_;
};

Q4.

Add a contains(char c) const method to String that returns true if c appears anywhere in the string.

// Add to String's public interface:
bool contains(char c) const;

// Implement:
bool String::contains(char c) const {
    // TODO
}

// String s("hello");
// s.contains('e') → true
// s.contains('z') → false

Q5.

Write a Point class representing a 2D point with double coordinates. It should have a constructor taking x and y, const getters x() and y(), a const method distance_from_origin() returning sqrt(x² + y²), and a method translate(double dx, double dy) that shifts the point in place.

#include <iostream>
#include <cmath>

class Point {
    // TODO
};

int main() {
    Point p(3.0, 4.0);
    std::cout << p.x() << " " << p.y() << "\n";   // 3 4
    std::cout << p.distance_from_origin() << "\n"; // 5

    p.translate(1.0, 0.0);
    std::cout << p.x() << " " << p.y() << "\n";   // 4 4

    return 0;
}

Practice Problems