Show Lecture.StructAndClass as a slide show.
CS253 Struct And Class
- struct is the old C way of doing things.
- In C, there were no methods—only data members.
- A struct is still useful for holding unadorned data.
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 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)
};
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
- In
Point::Point()
, this has type Point *
.
- In the const method
Point::radius()
, this has type const Point *
.
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”!
s
is private
- const reference argument
- member initialization
- const accessor (getter)
operator<<
not a method
- half-indent for
public:
- return ostream reference
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:
- caching a value that is expensive to obtain
- counting instances
- timestamping access for performance measuring
class Process {
public:
pid_t get_pid() const {
return getpid(); // assume this is slow
}
};
Process p;
cout << p.get_pid() << ' ' << p.get_pid() << '\n';
863931 863931
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.
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';
863932 863932
That works, but always calls getpid(). What if we never
need the process id? We called getpid() for no reason!
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';
863933 863933
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;
};
- All the methods of
class Bar
can access our data.
- So can the method
zulu(int) const
of the class Zip
.
- So can the function (not method)
alpha()
.
- So can the function
operator<<(ostream &, const Foo &)
.
Restrictions
- Friendship is offered, not demanded
- Friendship is not symmetric
- Friendship is not transitive
- If
E
& F
are friends, and F
& G
are friends,
then E
& G
are strangers.
- Friendship is not inherited
- If
H
is I
’s friend, and I
is the parent of J
,
then H
& J
are strangers.
- Inheritance does not imply friendship
- If
K
is L
’s parent, then K
& L
are strangers.
Don’t go nuts
- C++ programmers tend to fall in love with friend declarations,
and overuse them.
- friend should be your last tool, not your first one.
- Minimize access: prefer method friendship to class friendship.
- Create accessors (getters) and mutators (setters) instead.
- If you’re using friend declarations to avoid the overhead
of method calls, then you have no faith in current compilers.
They’re quite good at inlining methods.
Counting
- Imagine that, for some reason, we wanted to keep track of how
many instances of a class are extant.
- We’d need a counter.
- Every time a ctor is called, increment the counter.
- Every time a dtor is called, decrement the counter.
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.
A static member variable:
- has class scope
- can be public, protected, or private
- is shared between all instances of this class
- must be externally defined, typically in the implementation file.
A static method:
- has class scope
- can be public, protected, or private
- can be called without an instance of the class
- can’t access non-static data members, because those go with instances
- can’t call non-static data methods, because those go with instances.
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.”
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
- Yeah, but …, that’s stupid.
- Nobody would ever declare a
const Ratio
like that.
- Fair enough.
- Let’s look at a realistic scenario.
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.
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
- Some students think, “figuring out which methods should be
const is too much work; I’ll add const later”.
- This is a disaster in the making.
- Half an hour before the homework is due, the student decides
that it’s const time.
- The student adds const to a single method.
- This causes problems with all the non-const methods called by the
newly-const method.
- Making those methods const causes problems with the
non-const methods that they call.
- The student gives up, and turns in uncompilable code.
- The student remembers only that it’s all const’s fault,
and vows never to use const again.
Good Strategy
- If, instead, the student had declared methods const
as they were written, then there wouldn’t be an
“OMG, I’m doomed” moment.
class Foo {
public:
constexpr int bar() { return 42; }
};
Foo f;
cout << f.bar();
42
- I would’ve thought that the constexpr would go after the
()
.
Too bad for me!
- Think of the function returning a constexpr int,
that is, an int that can be computed at compile-time.
- This means that the compiler must calculate this value
at compile-time.
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()’
- constexpr means “Listen, compiler: you must execute this
function at compile-time (given constexpr arguments).
If you can’t, refuse to compile.”
- Hence, the compiler verifies your claim that the function is
doable at compile-time. Think of it as an assertion.
Temporary files
- Often, in programming, you need a temporary file, with a unique name.
- In Linux, these typically reside in the directory
/tmp
.
- Give it a meaningful name, in case of a misbehaving program.
- Two people might run your program simultaneously,
but they can’t use the same temporary file.
- Functions such as mkstemp help create a unique filename.
- Remove the file, even if your code takes an early return
or an exception is caught.
- A class can help with that—dtors love doing cleanup.
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-tOGam6
- Six alphanumerics means more than 56 billion possible filenames.
- Ctor creates a temporary file; dtor destroys it.
- The method
.name()
gets the path of the temporary file.
- Really, that’s the only thing that you do with it.
- Can we make easier to use?
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-2UzGRR
We replaced .name()
with
an operator string
method.
Just treat it like a std::string, and it works!