Lecture 3: Class Design

1 Classes

  • Classes are a representation of a custom-defined type.
  • Classes consist of:
    • An interface: What operations and variables are available when using this class.
    • An implementation: The definition of how things are done.

2 A note about the lecture material

From this point on, I will use the following convention:

  • I will put namespace std in .cpp files only.
  • Sometimes, I may use it in header files. This is not good practice, but it would save up space for presentation in class. I recommend not using using namespace std in the headers, that may cause clashes with other libraries that define their version of one of vector, pair, sort and tons of other things.

3 A note about C++ structs

  • structs and classes in C++ are exactly the same except for the following:
    • Struct members are set to public by default.
    • Class members are set to private by default.

4 Example - definition of a class representing a Person

CXX=clang++
CXXFLAGS=-std=c++17 -Wall

# Putting `main` as the first rule, so running `make` will be same as running `main`

main: main.o Person.o
        ${CXX} -o $@ ${CXXFLAGS} $^

main.o: main.cpp Person.h
        ${CXX} -o $@ -c ${CXXFLAGS} main.cpp

Person.o: Person.cpp Person.h
        ${CXX} -o $@ -c ${CXXFLAGS} Person.cpp

.PHONY: clean

clean:
        rm -f *.o main
// Person.h
#ifndef PERSON_H
#define PERSON_H

#include <string>
// For uint8_t
#include <cstdint>

// Interface for class representing a Person

class Person {
public:
  Person();           // default constructor
  Person(string name, uint8_t age);   // overloaded constructor
  Person(Person& person);     // copy constructor
  // Q: is making name/age private useful here?
  // why? why not?
  string getName() const;     // accessor / getter
  uint8_t getAge() const;         // accessor / getter
  void setName(string name);  // mutator / setter
  void setAge(uint8_t age);       // mutator / setter

private:
  string name;
  // This type is an Unsigned INTeger with 8 bytes (representing
  // values 0--255)
  uint8_t age;
};

#endif

Note: You should not use using namespace in your header files. This forces consumers of the class to use the namespace, which may not be intended or expected.

// Person.cpp
#include <iostream>
#include <string>
#include "Person.h"

using namespace std;

// default constructor
Person::Person() {
    name = "-";
    age = 0;
    cout << "Default Constructor" << endl;
}

// constructor overloading
Person::Person(string name, uint8_t age) {
    this->name = name;
    this->age = age;
}

// copy constructor
Person::Person(Person& person) {
    name = person.getName();
    age = person.getAge();
    cout << "copy constructor" << endl;
}

string Person::getName() const { return name; }

uint8_t Person::getAge() const { return age; }

void Person::setName(string name) { this->name = name; }

void Person::setAge(uint8_t age) { this->age = age; }
// main.cpp

#include <iostream>
#include "Person.h"

using namespace std;

int main() {
    Person p;
    return 0;
}

Note that we did not include <iostream> in Person.h because the interface does not depend on it even if the implementation does. Why does this matter?

  • Hint: what would happen if we were to recompile a file that includes Person.h?

5 Public vs. Private

  • The variables and functions declared as private cannot be referenced anywhere except within the class' functions.
  • The variables and functions declared as public can be referenced anywhere.
    • Why is this good?
      • Prevents unintended side-effects.
      • Hides complex details consumers of the class may not be concerned with.

6 Abstract Data Types (ADTs):

  • A data type where the programmers who use this class do not have access to the details of how the values and operations are implemented.
    • Also known as data hiding, data abstraction, and encapsulation.
    • Typically, a consumer of the class only needs to be aware of all the public fields.
    • Imagine needing to know / manipulate buffers in iostream in order to print "Hello World"!

7 Scope Resolution Operator ::

  • When a member function is defined, the definition must include the class name because there may be two or more classes that have member functions with the same name.
  • Without it, the compiler does not know which class' member function the definition is for.

8 Accessor and Mutator Functions

  • A function that simply returns private members' values are called accessor (getter) functions (i.e. they access the data).
  • A function that simply sets private members' values are called mutator (setter) functions (i.e. they change the data).

9 Constructors

  • A special kind of function with the same class name that it's defined in.
  • A constructor is called when an object of that class is declared.
  • Constructors are the only functions that do not have a return type.
  • Default constructors can be defined by you (which is good style).
  • If a default constructor is not defined, the compiler will generate one if no other constructor is defined! Otherwise it will not.

    • If you still want it to generate the default constructor along with your custom one, you can declare it with = defaultI know, it is a weird syntax:
    // Remember, struct is same as a class with public members by default
    struct TreeNode {
      std::vector<TreeNode> children_;
      std::string name_;
    
      // Special constructor when there are no children.
      //
      // Because we have this, the compiler no longer creates the default
      // constructor automatically.
      //
      // We will talk about `explicit` in a bit.
      explicit TreeNode(std::string name): name_(name) {}
    
      // Tell the compiler to create the default constructor
      TreeNode() = default;
    }
    

    You can also force the compiler to delete the copy constructor for example, if you don't want your type to be copyable for some reason, you can write:

    TreeNode(const TreeNode&) = delete;
    

10 Copy Constructor

  • Take in an existing Object in a constructor's parameter and set all of its fields to the fields of the object, thus copying one object's fields to the current object.
  • Copy constructors must pass its parameter by reference…
  • If a copy constructor is not defined, then the compiler will generate one. However, this copy constructor only does a shallow copy if you are using pointers.
    • Think of it as copying the reference of memory, not the memory contents.
    • Two references may now share the same memory of an object…
  • Try it: Comment out the defined copy constructor and note that
Person s;
Person t = s;

still works. If the copy constructor is defined, then the assignment operator operator = will call the defined copy constructor. If the copy constructor is not defined, the default copy constructor is used.

10.1 Shallow Copy illustration

// modify class Person.h definition with vector v
public:
std::vector<std::string>* getAliases() const;

private:
// DO NOT DO THIS IN PRACTICE, THERE IS NO POINT OF vector*
std::vector<std::string>* v;
// modify constructor in Person.cpp to initialize vector
Person::Person() {
  name = "default name";
  age = 0;
  v = new vector{};
}

// Now we remember the aliases when changing name
void setName(std::string name) {
  v->push_back(name);
  this->name = name;
}

// Accessor function for the vector v
vector<std::string>* Person::getAliases() const {
  return v;
}
// main.cpp

// function to print out contents of the vector
//
// we use a reference (`&`) to not copy the vector unnecessarily
// we also use `const` to show that we are not going to mutate the value held by that reference.
template<class T>
void printVector(const vector<T>& v) {
  for (const T& x : v) {
    cout << x << endl;
  }
}

// in main()
Person s;
Person t = s; // Calling the default copy constructor
s.setName("John Doe");
t.setName("Matt Smith");
printVector(*s.getAliases()); // John Doe; Matt Smith
printVector(*t.getAliases()); // John Doe; Matt Smith
  • Vector is shared between two objects due to the shallow copy!
  • One way to do a "deep copy" of the vector is
Person::Person(Person& person) {
  name = person.getName();
  age = person.getAge();
  // deep copy by calling std::vector's copy constructor
  v = new vector<std::string>(*person.getAliases());
  cout << "copy constructor" << endl;
}

11 Example of Overloading the Assignment Operator

// Person.h
Person& operator=(const Person& rhs);
// Person.cpp
Person& Person::operator=(const Person& rhs) {
    cout << "overloaded assignment operator" << endl;

    // check self-assignment, p1 = p1
    // so that we won't clear the vector unnecessarily
    if (this == &rhs) {
        return *this;
    }
    this->name = rhs.name;
    this->age = rhs.age;

    // deep copy by using std::vector's assignment operator
    *this->v = *rhs.v;
    return *this;
}

12 Copy Constructor vs. Assignment Operator

  • The copy constructor is invoked when the object does not exist yet and you're trying to assign an existing object to a new object.
  • Example:
int main() {
    Person s;
    cout << "address of s = " << &s << endl;
    Person t = s; // copy constructor
    cout << "address of t = " << &t << endl;
    cout << "---" << endl;
    Person a;
    Person b;
    a = b;  // overloaded assignment operator
}

13 Destructor

  • A special member function that is called when a reference to an object
    • Goes out of scope.
    • Or a pointer to the object is called with delete.
  • Example
// Person.h
~Person();  // destructor
// Person.cpp
Person::~Person() {
    delete v; // delete vector that should exist on the heap. Again,
              // allocating vectors using `new` is a bad idea.
    cout << "deleted: v" << endl;
}

14 Example illustrating destructor call when exiting function

// main.cpp
#include <iostream>
#include "Person.h"

using namespace std;

void f() {
    Person* p = new Person(); // default constructor assigns object on the heap
    delete p;   // manually call default constructor
                // Memory leak in the heap if call is not made.

    // Person p; // default constructor on the stack
    // when function exits, invokes destructor for objects on the stack.
}

int main() {
    f();
    cout << "exiting main..." << endl;
    return 0;
}

15 Big Three (Rule of Three)

  • Rule of thumb: If you are implementing your own version of a copy constructor, destructor, or assignment operator, then you should implement all three.
  • Important when manually managing memory on the heap or other resources (files, network connections).
  • A good link illustrating the Big Three … in the context of programming a video game :)
  • A good reference that explains it

15.1 Rule of Five

Later in the quarter, we may talk about moving objects rather than copying them, for the sake of efficiency (and sometimes correctness!). There is a constructor for moving objects called move constructor. Rule of Five is an extension of Rule of Three, where you would also need to define the constructor and assignment operator for moving. The link above also explains it.

15.2 Rule of Zero

Because Rule of Three/Five encourages us to do work by defining 3 or 5 functions we need, there is this idea: avoid re-defining copy constructor, destructor, and assignment operator and use the given defaults whenever you can. This is Rule of Zero.

16 Other reference material

  • Sized integer types in <cstdint> like uint8_t.

Footnotes:

1

I know, it is a weird syntax

Author: Mehmet Emre

Created:

The material for this class is based on Prof. Richert Wang's material for CS 32