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, 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 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!
};
The special variable this is a pointer (not a reference,
as in Java) to the current object. It’s rarely needed.
Here is an unnecessary use of this:
class Point {
public:
Point(int a, int b) { x = a; y = b; }
double radius() const { return sqrt(x*x + y*this->y); }
private:
int x, y;
};
Point p(300,400);
cout << p.radius() << '\n';
500
Inside Point::radius()
, this is of type const Point *
,
because the method is const.
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
- const accessor (getter)
operator<<
not a method
- half-indent for
public:
- return ostream reference
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);
}
};
int main() {
const Ratio f(355,113);
cout << f.get_real() << '\n';
}
c.cc:12: error: passing 'const Ratio' as 'this' argument discards qualifiers
Oops—can’t call a non-const method on a const object.
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
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, 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,
but they can’t use the same temporary file.
- 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[18] = "/tmp/cs253-XXXXXX";
};
Tempfile t;
const string fname = t.tempname();
cout << fname << '\n';
/tmp/cs253-cGD6lJ
- Six alphanumerics means more than 56 billion possible filenames.
- 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[18] = "/tmp/cs253-XXXXXX";
};
Tempfile t;
const string fname = t;
cout << fname << '\n';
/tmp/cs253-oNsmAD
Just treat it like a std::string, and it works!