A clarification of some terminology:
X
, you can create objects of type X
by defining a local variable, or by using the new
operator, or by any of the usual ways we create objects in programs.X
and an object whose type is X
is very important. An object of type X
is referred to as an instance of X
, so restating the previous sentence: the difference between the class X
and instances of X
is very important.If you create a class Midshipman
, you will probably have a data member name
. Each instance of class Midshipman
has its own name
, so that two different objects of type Midshipman
can (and usually do) have different name
s. Sometimes, however, we want to define data that is shared amongst all instances of the class - i.e. is global - rather than have each instance carry around its own copy of the data. For example, if you wanted to add the Supe's name as data belonging to the class, there should only be one Supe's name, and it should be shared by all instances of Midshipman
. Imagine having each instance carry around his own copy of the Supe's name. It'd be possible for two different instances of Midshipman
to disagree on who the Supe is!
This lecture will discuss how data, functions and types that are global to a class may be defined.
A class variable is a piece of data that is global to a class. The ubiquitous keyword static
, which means so many different things in so many different contexts, is used to declare a class variable. So, if a variable var is declared in the definition of class cls with the keyword static
, you've essentially declared a global variable whose name is cls::var (inside the class you may simply refer to var), but which obeys the public/private access rules for classes.
Since class variables are very similar to global variables, you should treat them with the same level of apprehension, and only use them if you absolutely need to use them. There are likely other options available that will produce your desired result.
The syntax for creating and using class variables, as well as the reasons for wanting one in the first place, are probably best seen in an example. Consider our constant friend, the class Point
. Suppose you wanted to keep a count of how many Point
objects are alive at any point in the program - perhaps because you suspected you had a memory leak in a program (Quick: what does that mean for our usage of new
and delete
?). In the following version of the Point
class, we declare a class variable count
and use it to track the number of live Point
s. Every time a Point
dies, we'll print out the number remaining.
ex1-point.h | ex1-point.cpp | ex1-main.cpp |
|
|
|
Below is a our program again that uses our Point
class, along side sample run of the program. Our facility for tracking the number of live Point
s does indeed find a memory leak, meaning that we don't destroy all the Point
s our program creates. Only one Point
dies before the program ends, two others remain and are never destroyed! Can you see why this happens?
In fact, if you run this code you'll notice that we get to the return
statement just fine and the program attempts to clean up our memory after exiting main
since the other two points still remain. However, the pointer to the Point
array is no longer valid since we called delete
on it, resulting in an invalid memory access and an error even though the program worked fine!
ex1-main.cpp (again) | Sample run of program |
|
> g++ -o ex1 ex1-main.cpp ex1-point.cpp > ./ex1 (1,3) (-2,0.5) (0,4.4) (1,3) (-2,0.5) (0,4.4) A Point dies, now there are 2 *** Error in `./ex1': munmap_chunk(): invalid pointer: 0x0000000001045c28 *** ======= Backtrace: ========= /lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f7e4d1077e5] /lib/x86_64-linux-gnu/libc.so.6(cfree+0x1a8)[0x7f7e4d114698] ./ex1[0x400d76] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f7e4d0b0830] ./ex1[0x400bd9] ======= Memory map: ======== 00400000-00402000 r-xp 00000000 00:38 3201 ... ... ... Aborted (core dumped) > |
cout
statements so we can see what's happening:
What did you observe?
Functions that are global to a class - i.e. that belong to the class but do not belong to any one instance in the same way as class variables - may be defined using our favorite keyword: static
. A class function is essentially just a stand-alone function that:
::
, followed by the name you've given the function.
operator if an object (instance) does exist
Point
class above that allows the user of the class to find out the current count of alive Point
s.We've seen that variables and functions that are global within a class can be defined. They're much like their truly global counterparts, except that their full names include classname::, and that access to them can be controlled via the public/private mechanism. Variables ... functions ... the only thing that's left are classes. Now we'll see that classes that are global to a class may also be defined. Such a nested class differs from a regular class in exactly the same two ways. This concept is really nice.
Suppose the definition of a class ClsB
is nested within the definition of a class ClsA
, as in the following:
class ClsA {
private:
class ClsB {
public:
int x;
private:
int u;
};
public:
ClsB S;
friend int f(ClsA G);
};
The name ClsB
has no meaning on its own outside of the class ClsA
. The true, full name of the class is actually ClsA::ClsB
, i.e. "the class ClsB
occurring within the class ClsA
". If ClsB
is really just a helper class for ClsA
, then it probably shouldn't have an identity on its own, so using nested classes makes sense. It can also really help with walling off the user from the implementation, because the public/private access specifiers in ClsA
can make ClsB
inaccessible to the outside world.
Within ClsA
, member data and functions of ClsB
are accessible according to the same rules as if ClsB
had been declared outside of any class definitions. Outside ClsB
, accessibility of data in ClsB
requires first applying the access rules for ClsA
(i.e. is the definition of class ClsB
accessible to me?) and then applying the accessibility rules for the particular member function or data in ClsB
. For example, in the main
function
int main() {
ClsA G; // legal
G.S.u = 0; // access denied! u is private in ClsB
G.S.x = 0; // legal: S is a public instance of ClsB, and x is public in ClsB
ClsA::ClsB H; // access denied! ClsB is private in ClsA, so you can't create instances of ClsB outside of ClsA
return 0;
}
However, inside the friend function f
of ClsA
, things are different. For example:
int f(ClsA G) {
G.S.u = 0; // access denied! u is private in ClsB
G.S.x = 0; // legal
ClsA::ClsB H; // legal because f is declared as a friend within ClsA, so it can see ClsA's private parts (i.e. ClsB)
return 0;
}
The most obvious case of a class serving merely as a helper to another class, is the case of a LinkedList, where the Node
class really only exists to make up the components of a LinkedList.
Now, suppose we create our List
class to include a function which adds a value to the list and another which determines if a value exists in the list. Also, List
will contain a nested and private Node
class which a user will naturally have no access to. Therefore, we have the following elegant version of this class:
ex2-list.h | ex2-list.cpp |
|
|
The file ex2-main.cpp (below) has a simple driver program that uses the List
defined above. In this version of List
, the user is actually unable to access any part of the class Node
, which brings us closer to our goal of separating interface and implementation.
ex2-main.cpp
/* This program reads numbers and
prints out the how many were
read,ignoring duplicates */
#include "ex2-list.h"
int main() {
// Initialize
List L;
int n, count = 0;
// Read #s
while(cin >> n)
if (!L.contains(n))
{
L.add(n);
count++;
}
// Print results
cout << count
<< " unique numbers"
<< endl;
return 0;
}