Command Palette

Search for a command to run...

MTH 4300

Lecture 12

Why Overload Operators?

Operator overloading lets user-defined types use the same syntax as built-in types. Without it, working with String is verbose:

Without operators
String a("foo"), b("bar");
String c = concat(a, b);
if (equal(a, c)) { /* ... */ }
print_to(std::cout, a);

With overloaded operators, the same code is natural:

With operators
String a("foo"), b("bar");
String c = a + b;
if (a == c) { /* ... */ }
std::cout << a << "\n";

Member vs Non-Member Operators

An operator can be a member function or a free (non-member) function:

OperatorFormReason
[], =, (), ->must be memberlanguage requirement
<<, >>must be non-memberleft operand is ostream, not your class
+, ==, !=, <eithernon-member preferred for symmetry

Member operators receive this as the implicit left operand. Non-member operators take both operands explicitly. A non-member that needs access to private members is declared a friend:

Friend declaration
class String {
    friend std::ostream& operator<<(std::ostream& os, const String& s);
    friend String        operator+(const String& lhs, const String& rhs);
    friend bool          operator==(const String& lhs, const String& rhs);
};

Subscript Operator (operator[])

Provide both a const and a non-const overload so bracket access works on both mutable and const String objects:

operator[]
// In the class (member):
char&       operator[](size_t i);
const char& operator[](size_t i) const;

// Definitions:
char& String::operator[](size_t i) {
    return data_[i];
}

const char& String::operator[](size_t i) const {
    return data_[i];
}

The non-const version returns a reference so callers can write s[0] = 'H'.

Like std::string, operator[] performs no bounds check. Use at() when you want a guaranteed exception on out-of-range access.

Equality Operators (operator== and operator!=)

Comparing two strings means checking length and then contents:

operator== and operator!=
// Non-member friends:
bool operator==(const String& lhs, const String& rhs) {
    if (lhs.size() != rhs.size()) return false;
    return std::strcmp(lhs.c_str(), rhs.c_str()) == 0;
}

bool operator!=(const String& lhs, const String& rhs) {
    return !(lhs == rhs);
}

std::strcmp (from <cstring>) returns 0 if two C-strings are equal, negative if lhs < rhs, positive if lhs > rhs.

Stream Output (operator<<)

operator<< must be a non-member — the left operand is std::ostream, not String. Return std::ostream& to allow chaining (std::cout << a << b):

operator<<
std::ostream& operator<<(std::ostream& os, const String& s) {
    os << s.c_str();
    return os;
}

Concatenation (operator+)

operator+ creates a new String containing both — it does not modify either operand:

operator+
String operator+(const String& lhs, const String& rhs) {
    size_t new_len = lhs.size() + rhs.size();
    char*  buf     = new char[new_len + 1];

    std::strcpy(buf, lhs.c_str());
    std::strcat(buf, rhs.c_str()); // appends rhs onto the end of buf

    String result(buf);
    delete[] buf;
    return result;
}

std::strcat (from <cstring>) appends the second C-string onto the end of the first.

In-Place Append (operator+=)

operator+= modifies this in place and returns *this:

operator+=
String& String::operator+=(const String& other) {
    size_t new_len  = len_ + other.len_;
    char*  new_data = new char[new_len + 1];

    std::strcpy(new_data, data_);
    std::strcat(new_data, other.data_);

    delete[] data_;
    data_ = new_data;
    len_  = new_len;

    return *this;
}

With operator+= defined, operator+ can delegate to it:

operator+ via operator+=
String operator+(String lhs, const String& rhs) { // lhs taken by value (copy)
    lhs += rhs;
    return lhs;
}

String — Full Declaration Through Lecture 12

String class — Lectures 9–12
class String {
public:
    String();
    explicit String(const char* s);
    ~String();

    String(const String& other);
    String& operator=(const String& other);

    String(String&& other) noexcept;
    String& operator=(String&& other) noexcept;

    size_t size()  const;
    bool   empty() const;
    char   at(size_t i) const;
    const char* c_str() const;

    char&       operator[](size_t i);
    const char& operator[](size_t i) const;
    String&     operator+=(const String& other);

    friend bool          operator==(const String& lhs, const String& rhs);
    friend bool          operator!=(const String& lhs, const String& rhs);
    friend String        operator+(String lhs, const String& rhs);
    friend std::ostream& operator<<(std::ostream& os, const String& s);

private:
    size_t len_;
    char*  data_;
};

Exercises

Q1.

Without running the code, predict what this program prints.

#include <iostream>

int main() {
    String a("foo");
    String b("bar");
    String c = a + b;

    std::cout << c        << "\n"; // line A
    std::cout << c.size() << "\n"; // line B
    std::cout << c[0]     << "\n"; // line C

    c[0] = 'F';
    std::cout << c << "\n";        // line D

    return 0;
}

Q2.

Without running the code, predict what this program prints.

#include <iostream>

int main() {
    String a("hello");
    String b("hello");
    String c("world");

    std::cout << (a == b) << "\n"; // line A
    std::cout << (a == c) << "\n"; // line B
    std::cout << (a != c) << "\n"; // line C

    a += String(" world");
    std::cout << a << "\n";        // line D

    return 0;
}

Q3.

Each snippet has a bug related to operator overloading. Identify what's wrong.

// Snippet A — operator<< as member
void String::operator<<(std::ostream& os) {
    os << data_;
}

// Snippet B — operator= returning by value
String String::operator=(const String& other) {
    if (this == &other) return *this;
    delete[] data_;
    len_  = other.len_;
    data_ = new char[len_ + 1];
    std::strcpy(data_, other.data_);
    return *this;
}

// Snippet C — comparing pointers instead of contents
bool String::operator==(const String& other) {
    return data_ == other.data_;
}

// Snippet D — operator+ modifying lhs
String& String::operator+(const String& other) {
    std::strcat(data_, other.data_); // appends into this
    return *this;
}

Q4.

Implement operator< for String that compares two strings lexicographically.

// Add as friend in the class:
friend bool operator<(const String& lhs, const String& rhs);

// Implement (std::strcmp returns negative if lhs < rhs):
bool operator<(const String& lhs, const String& rhs) {
    // TODO
}

// String a("apple"), b("banana");
// a < b → true
// b < a → false
// a < a → false

Q5.

Using only the operators defined on String so far (+, ==, <<, []), write a function is_anagram(const String& a, const String& b) that returns true if the two strings contain exactly the same characters in any order. You may use a plain int freq[256] = {} array for character frequency counts.

#include <iostream>

bool is_anagram(const String& a, const String& b) {
    // TODO
}

int main() {
    std::cout << is_anagram(String("listen"), String("silent")) << "\n"; // 1
    std::cout << is_anagram(String("hello"),  String("world"))  << "\n"; // 0
    std::cout << is_anagram(String("abc"),    String("ab"))     << "\n"; // 0

    return 0;
}

Practice Problems