CS253: Software Development with C++

Spring 2023

Struct And Class

Show Lecture.StructAndClass as a slide show.

CS253 Struct And Class

made at imgflip.com

struct

struct Point {
    int x[100], y;
};
Point p;
p.x[90] = 12345;
p.y = 67890;
cout << p.x[90] << ',' << p.y << '\n';
12345,67890

class

class Point {
    int x, y;                               // Hands off!
  public:
    Point(int a, int b) { x = a; y = b; }   // constructor (alias ctor)
    int get_x() const { return x; }         // accessor (getter)
    int get_y() const { return y; }         // accessor (getter)
    void go_right() { x++; }                // mutator (setter)
};

this

The rarely needed special variable this is a pointer (not a reference, as in Java) to the current object. Member variables have class scope, so methods can access them without this->:

class Point {
  public:
    Point(int a, int b) { x = a; y = b; }
    double radius() const { return sqrt(x*x + y*this->y); } // 🦡 unnecessary this->
  private:
    int x, y;
};
Point p(300,400);
cout << p.radius() << '\n';
500

Output

To make a class work for output, define operator<< appropriately:

class Point {
  public:
    Point(int a, int b) { x = a; y = b; }   // ctor
    int get_x() const { return x; }         // accessor (getter)
    int get_y() const { return y; }         // accessor (getter)
    void go_right() { x++; }                // mutator (setter)
  private:                                  // Mine!
    int x, y;
};

ostream& operator<<(ostream &out, const Point &p) {
    return out << p.get_x() << ',' << p.get_y();
}

int main() {
    Point p(12, 34);
    cout << p << '\n';      // invoke operator<<
}
12,34

Example

class Quoted {
    string s;
  public:
    Quoted(const string &word) : s(word) { }
    string get() const { return "“" + s + "”"; }
};

ostream& operator<<(ostream &os,
                    const Quoted &q) {
    return os << q.get();
}

int main() {
    Quoted name("Bob");
    cout << "Hi, " << name << "!\n";
}
Hi, “Bob”!

mutable

mutable is a hack. It indicates that, even if a method is const, and hence should not modify data members, it’s ok to modify this data member. mutable is used when you want to change a data member without really changing the state of the object:

mutable #1

class Process {
  public:
    pid_t get_pid() const {
        return getpid(); // assume this is slow
    }
};

Process p;
cout << p.get_pid() << ' ' << p.get_pid() << '\n';
3086597 3086597

This is the straightforward solution. However, if getpid() is slow, then we’re calling the slow function twice. The process id (pid) doesn’t change! We should only call getpid() once.

mutable #2

class Process {
    const pid_t pid = getpid();
  public:
    pid_t get_pid() const {
        return pid;
    }
};

Process p;
cout << p.get_pid() << ' ' << p.get_pid() << '\n';
3086598 3086598

That works, but always calls getpid(). What if we never need the process id? We called getpid() for no reason!

mutable #3

class Process {
    mutable pid_t pid = -1;
  public:
    pid_t get_pid() const {
        if (pid == -1)
            pid = getpid(); // assume this is slow
        return pid;
    }
};

Process p;
cout << p.get_pid() << ' ' << p.get_pid() << '\n';
3086599 3086599

This is the solution. getpid() is called once, at most, and not called at all if the process ID is never required. (Arguably, a local static variable inside the method would be simpler.)

methods: in-line and not

Methods are declared in the class, but can be defined inside or outside of the class.

class Quoted {
    string s;
  public:
    Quoted(const string &word) : s(word) { }
    string get() const; // declaration only
};
string Quoted::get() const {
    return "“" + s + "”";
}
ostream& operator<<(ostream &os, const Quoted &rhs) {
    return os << rhs.get();
}

int main() {
    Quoted name("Slartibartfast");
    cout << "I am " << name << ".\n";
}
I am “Slartibartfast”.

Protection

Protection

class Foo {     // A class, with a variable of each type
  public:
    int pub;    // I’m public!
  private:
    int priv;   // I’m private!
  protected:
    int prot;   // I’m a little teapot, short & stout.
};

int main() {
    Foo f;

    f.pub = 1;          // great
    // f.priv = 2;      // nope
    // f.prot = 2;      // nope

    return f.pub;
}

Friends

Friend Declarations

One class can declare another class/method/function to be its friend:

class Foo {
    friend class Bar;
    friend double Zip::zulu(int) const;
    friend int alpha();
    friend std::ostream& operator<<(std::ostream &os, const Foo &);
  private:
    int x,y;
};

Restrictions

Don’t go nuts

Counting

Try #1

class Foo {
  public:
    Foo() { counter++; }
    ~Foo() { counter--; }
    int get_count() const { return counter; }
  private:
    int counter=0;  // 🦡
};

int main() {
    Foo a, b, c, d, e;
    cout << e.get_count() << '\n';
}
1
Why doesn’t this work?

Each instance of Foo has its own counter, and each one gets initialized to zero, then incremented to one.

Try #2

int counter=0;  // 🦡

class Foo {
  public:
    Foo() { counter++; }
    ~Foo() { counter--; }
    int get_count() const { return counter; }
};

int main() {
    Foo a, b, c, d, e;
    cout << e.get_count() << '\n';
}
5

That works, but it has an evil global variable. 👹

Try #3

class Foo {
  public:
    Foo() { counter++; }
    ~Foo() { counter--; }
    int get_count() const { return counter; }
  private:
    static int counter=0;  // 🦡
};

int main() {
    Foo a, b, c, d, e;
    cout << e.get_count() << '\n';
}
c.cc:7: error: ISO C++ forbids in-class initialization of non-const static 
   member ‘Foo::counter’

That failed.

Try #4

class Foo {
  public:
    Foo() { counter++; }
    ~Foo() { counter--; }
    int get_count() const { return counter; }
  private:
    static int counter;
};
int Foo::counter = 0;

int main() {
    Foo a, b, c, d, e;
    cout << e.get_count() << '\n';
}
5

There we go! Only a single, shared, counter, with class scope.

static class variables

A static member variable:

static methods

A static method:

const methods

class Ratio {
    int top, bottom;
  public:
    Ratio(int a, int b) : top(a), bottom(b) { }
    double get_real() {  // 🦡
        return top / double(bottom);
    }
};

const Ratio f(355,113);
cout << f.get_real() << '\n';
c.cc:11: error: passing ‘const main()::Ratio’ as ‘this’ argument 
   discards qualifiers

Oops—can’t call a non-const method on a const object.
“But get_real() doesn’t change anything!”
“Then make it const.”

const methods

class Ratio {
    int top, bottom;
  public:
    Ratio(int a, int b) : top(a), bottom(b) { }
    double get_real() const {
        return top / double(bottom);
    }
};

int main() {
    const Ratio f(355,113);
    cout << f.get_real() << '\n';
}
3.14159

Making Ratio::get_real() const fixed it. It also properly indicates that get_real() doesn’t change the object.

Rationale

const methods

class Ratio {
    int top, bottom;
  public:
    Ratio(int a, int b) : top(a), bottom(b) { }
    double get_real() const {
        return top / double(bottom);
    }
};

void show_ratio(Ratio r) {
    cout << r.get_real() << '\n';
}

int main() {
    Ratio f(355,113);
    show_ratio(f);
}
3.14159

That call to show_ratio copies an object by value. Expensive! 😱 Well, OK, it’s only two ints—big deal. If the object had a lot of data, or used dynamic memory, then copying would be expensive.

const methods

class Ratio {
    int top, bottom;
  public:
    Ratio(int a, int b) : top(a), bottom(b) { }
    double get_real() const {
        return top / double(bottom);
    }
};

void show_ratio(const Ratio &r) {
    cout << r.get_real() << '\n';
}

int main() {
    Ratio f(355,113);
    show_ratio(f);
}
3.14159

Look! A const Ratio! r is now const, so get_real() must be const.

Implementation

The compiler’s implementation of a const method is simple; it regards the hidden implicit pointer argument this as pointing to a const object, which explains the message:

class Foo {
  public:
    void zip() {}  // 🦡
};

int main() {
    const Foo f;
    f.zip();
}
c.cc:8: error: passing ‘const Foo’ as ‘this’ argument discards 
   qualifiers

In Foo::zip(), this is type Foo *, which can’t point to const Foo f.

Poor Strategy

Good Strategy

constexpr variable

constexpr methods

class Foo {
  public:
    constexpr int bar() { return 42; }
};

Foo f;
cout << f.bar();
42

constexpr methods

class Foo {
  public:
    constexpr int bar() { return getpid(); }  // 🦡
};

Foo f;
cout << f.bar();
c.cc:3: error: call to non-‘constexpr’ function ‘__pid_t getpid()’

Temporary files

Conversion methods

class Tempfile {
  public:
    Tempfile() { close(mkstemp(path)); }
    ~Tempfile() { remove(path); }
    string name() const { return path; }
  private:
    char path[18] = "/tmp/cs253-XXXXXX";
};

Tempfile t;
const string fname = t.name();
cout << fname << '\n';
/tmp/cs253-n7y2GY

Conversion methods

class Tempfile {
  public:
    Tempfile() { close(mkstemp(path)); }
    ~Tempfile() { remove(path); }
    operator string() const { return path; }
  private:
    char path[18] = "/tmp/cs253-XXXXXX";
};

Tempfile t;
const string fname = t;
cout << fname << '\n';
/tmp/cs253-d7coa6

We replaced .name() with an operator string method.

Just treat it like a std::string, and it works!