Show Lecture.StructAndClass as a slide show.
CS253 Struct And Class
struct
struct
is the old C way of doing things.
- In C, there were no methods—only data members.
- Structs are still useful for holding unadorned data.
struct Point {
int x, y;
};
Point p;
cout << p.x << '\n';
p.x = 8;
p.y = 9;
cout << p.x << ',' << p.y << '\n';
c.cc:5: warning: 'p.main()::Point::x' is used uninitialized in this function
0
8,9
class
- Classes have methods and data. Data is usually private.
- A class should have public methods to be useful, (e.g., a ctor)
but private methods are also common.
public
& private
define sections of the class.
class Point {
public:
Point(int a, int b) { x = a; y = b; } // constructor (alias ctor)
int get_x() const { return x; } // accessor
int get_y() const { return y; } // accessor
void go_right() { x++; } // mutator
private:
int x, y; // Hands off!
};
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
int get_y() const { return y; } // accessor
void go_right() { x++; } // mutator
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 &rhs) {
return os << rhs.get();
}
int main() {
Quoted name("Bob");
cout << "Hi, " << name << "!\n";
}
Hi, “Bob”!
s
is private
const
reference argument
- member initialization
operator<<
not a method
- half-indent for
public:
- return
ostream
reference
methods: in-line and not
Methods 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
- Class members (data & methods) can be
public
, private
,
or protected
.
- The default is
private
for a class
,
public
for a struct
.
public
: anyone can access them
private
: nobody except other methods of this class can access them
protected
: can be accessed by this class,
and by immediately derived class
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
class A
, via friend class B
,
offers friendship to class B
.
- Friendship is not symmetric
- If
C
is D
’s friend, then D
is not necessarily
C
’s friend.
- 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 often 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 (still exist).
- 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?
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
static 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
The scope of the static
global variable is only this file,
but that’s still too much.
Try #4
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 #5
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:
- has
class
scope
- can be
public
, protected
, or private
- is shared between all instances of this
class
- must be externally defined.
static
methods
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.
const
methods
class Ratio {
public:
Ratio(int a, int b) : top(a), bottom(b) { }
double get_real() {
return top / double(bottom);
}
private:
int top, bottom;
};
int main() {
const Ratio f(355,113);
cout << f.get_real() << '\n';
}
c.cc:13: error: passing 'const Ratio' as 'this' argument discards qualifiers
Oops—can’t call a non-const
method on a const
object.
const
methods
class Ratio {
public:
Ratio(int a, int b) : top(a), bottom(b) { }
double get_real() const {
return top / double(bottom);
}
private:
int top, 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.
const
methods
class Ratio {
public:
Ratio(int a, int b) : top(a), bottom(b) { }
double get_real() const {
return top / double(bottom);
}
private:
int top, 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!
😱
const
methods
class Ratio {
public:
Ratio(int a, int b) : top(a), bottom(b) { }
double get_real() const {
return top / double(bottom);
}
private:
int top, bottom;
};
void show_ratio(const Ratio &r) {
cout << r.get_real() << '\n';
}
int main() {
Ratio f(355,113);
show_ratio(f);
}
3.14159
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 this “OMG, I’m doomed” moment.
constexpr
variable
constexpr
methods
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!
- This means that the compiler must calculate this value
at compile-time.
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()'
constexpr
means “Listen, compiler: you must execute this
function at compile-time (given constexpr
arguments).
If you can’t, complain and die.”
- 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.
- Functions such as mkstemp help create a unique file.
- You should 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(name)); }
~Tempfile() { remove(name); }
string tempname() const { return name; }
private:
char name[32] = "/tmp/cs253-XXXXXX";
};
Tempfile t;
const string fname = t.tempname();
cout << fname << '\n';
/tmp/cs253-qwXerG
- Ctor creates a temporary file; dtor destroys it.
- The method
.tempname()
gets the name of the temporary file.
- Really, that’s the only thing that you do with it.
- Can we make this even easier?
Conversion methods
Let’s add an operator string
method:
class Tempfile {
public:
Tempfile() { close(mkstemp(name)); }
~Tempfile() { remove(name); }
operator string() const { return name; }
private:
char name[32] = "/tmp/cs253-XXXXXX";
};
Tempfile t;
const string fname = t;
cout << fname << '\n';
/tmp/cs253-lOavYr
Just treat it like a std::string
, and it works!