3 MyString Class

6 minute read

Published:

Header function: class definition with all member functions declared

  • Except for short functions (let compiler inline it)
  • Define them in the cpp file, prepended with MyString::

mystring.h

class MyString {
public:
	MyString();
	MyString(const char* p);
	~MyString();
	MyString(const MyString& s);             // copy constructor
	MyString& operator=(const MyString& s);  // copy assignment
	int length() const {return len;}

    friend MyString operator+(const MyString& s1, const MyString& s2);
    friend std::ostream& operator<<(std::ostream& os, const MyString& s);
    friend std::istream& operator>>(std::istream& is, MyString& s);
    char& operator[](int i);
    const char& operator[](int i) const;
    
private:
	char* data;
	int len;
};

Makefile

executables = test1 test2 test3
objects = mystring.o test1.o test2.o test3.o

.PHONY: default
default: $(executables)

$(executables): mystring.o
$(objects): mystring.h
  • default has all executables we want to build. Will get expanded to test1 test2 test3
  • For each $(executables) and $(objects) gets expanded to
test1: test1.o mystring.o
	g++ test1.o mystring.o -o test1
	
test1.o: test1.cpp mystring.h
	g++ -g -Wall -std=c++14 -c test1.cpp

The variables will be expanded. *.o and *.cpp ingredients are implied


Basic 4

Don’t forget the nullptr case, as well as allocating len + 1

#include "mystring.h"

int main() {
    using namespace std;
    MyString s1;
    MyString s2("hello");   
    MyString s3(s2);       // copy construct
    s1 = s2;               // copy assignment
    cout << s1 << "," << s2 << "," << s3 << endl;
}

Constructor

When you initialize "hello":

  • Type: char [6]
  • Value: pointer at .rodata, betweencode and data
    • However, string there will be immutable.
    • Need to allocate a copy
MyString::MyString(const char* p) {
	if (p) {
		len = strlen(p);
		data = new char[len + 1];
		strcpy(data, p);
	} else {/* allocate a null string */}
}
  • new char[6]: allocate 6 elements of char on heap
    • If new fails to allocate, throws exception
  • Can’t allocate on stack due to scope

If string is empty, allocating 1 byte is unfortunate. Why not make it NULL?

  • Creates an invariant property, better testing integrity
  • real std::string has a short (E. 32-byte) buffer to avoid heap allocation

Destructor

MyString::~MyString() {
	delete[] data;
}

Copy Constructor

Consider this code

void f(MyString s2)
	cout << s2;
	
int main() {
	MyString s1("hello");
	f(s1);
	cout << s1;
}

If copy constructor not defined, s2 will be copied on the stack, member-wise

  • When f returns, s2 goes away, and its destructor gets called!!!
  • s1.data deleted!!!
  • Shallow copy!

Need make every copy carry its own heap allocated string

MyString::MyString(const MyString& s) {
    len = s.len;
    data = new char[len+1];
    strcpy(data, s.data);
}

Copy Assignment

Need do a little more,

MyString& MyString::operator=(const MyString& rhs) {
	// if s1 = s1, DON'T change anything
    if (this == &rhs)    
        return *this;

    // first, deallocate memory that 'this' used to hold
    delete[] data;

    // same as copy constructor
    len = rhs.len;
    data = new char[len+1];
    strcpy(data, rhs.data);

    return *this;
}

What if I use *this == rhs?

  • Same result, but not efficient to compare all struct contents
  • Depends on operator==() definition

Rule of 3

  • If you implement any of destructor, copy constructor, and copy assignment, you probably need to do all 3 of them

Inline Functions

Entire body defined in mystring.h

  • Tell compiler to inline it
  • Compiler may refuse (E. long/recursive/virtual functions)

Operators

operator+()

operator+() is not a member function (no this pointer, no MyString::). It’s global

MyString operator+(const MyString& s1, const MyString& s2) {
    MyString temp;
    delete[] temp.data;

    temp.len = s1.len + s2.len;
    temp.data = new char[temp.len+1];
    strcpy(temp.data, s1.data);
    strcat(temp.data, s2.data);

    return temp;
}

How does it access private data?

  • Declared as friend in prototype
friend MyString operator+(const MyString& s1, const MyString& s2);

After return temp, a copy of temp is created, which goes into rhs for operator=()

Why is operator=() member, but operator+() global? Either works syntax-wise

  • operator=() modifies LFS, so make it member
  • lhs and rhs of operator+() are symmetrical, so make it global

(s1 + "world") also works

  • Compiler first looks for overload of operator+(MyString, char*)
  • Next, sees if can promote one of the mismatched argument into MyString
    • By invoking constructor MyString(char*)
      1. Finds a constructor of MyString that takes char*
      2. Constructs a temporary object from char*
      • Only works if operator+() is a global function "hello" + "world" doesn’t work
  • Preserves C behavior

operator<<

  • cout is std::ostream that represents stdout
  • Return it by reference
    • So associativity not violated
friend std::ostream& operator<<(std::ostream& os, const MyString& s);

Why not make it a member function of std::ostream? Why make it global?

  • Don’t want to redefine the code in std
std::ostream& operator<<(std::ostream& os, const MyString& s) {
    os << s.data;    // give os the char*
    return os;       // return BY REFERENCE
}

(cout << s1) << "something else";   // LHS is still cout

operator>>()

friend std::istream& operator>>(std::istream& is, MyString& s);

std::istream& operator>>(std::istream& is, MyString& s) {
    std::string temp;
    is >> temp;

    delete[] s.data;

    s.len = strlen(temp.c_str());
    s.data = new char[s.len+1];
    strcpy(s.data, temp.c_str());

    return is;
}

Cheated with std::string

  • Actually, need to read each character from stdin, and get whitespace right
  • .c_str gives the regular char*

operator[]()

Gives the i-th character

  • char& return, because need to write into the string

If MyString is const, both of the following would work:

const char& operator[](int i) const;
char& operator[](int i) const;
char& MyString::operator[](int i) {
    if (i < 0 || i >= len) {
        throw std::out_of_range{"MyString::op[]"};
    }
    return data[i];
}

operator[]() const

If this is const, all its members will be const

  • Cast away constness
  • *this has type const MyString*
  • When you dereference const MyString*, it becomes const MyString&
  • Cast to regular MyString&
  • Can invoke the regular operator[](), which returns a char&
  • Assigning char& into const char& is fine
const char& MyString::operator[](int i) const {
    // illustration of casting away constness
    return ((MyString&)*this)[i];
}

The cpp syntax is:

	return const_cast<MyString&>(*this)[i];
  • Also checks if *this is MyString& to begin with

Exception Handling

	throw std::out_of_range{"MyString::op[]"};
	
	// same as
	std::out_of:range ex{"MyString::op[]"};  // construct a temp object
	throw ex                                 // return by value
  • std::out_of_range is a type, constructs a temporary object ex, and returns by value

If not catched, and the exception goes out of main(), a library function will catch it, and terminate the program

	try {
		f1();    // function call that may throw an exception
	}
	catch (const out_of_range& e) {
		cout << e.what() << endl;
	}

Catch with const reference (if by value, has a copy construct…)

  • Then invoke the member function what()

If throwing an exception makes a function exit in the middle, the local variables will be properly destructed