friend
& const
& more Constructorsfriend
: Giving access to private members of a classA class can grant a specific function (or an entire other class) access to its private members (variables and functions) by using the keyword friend
in its class definition. Friendship must be granted, it cannot be taken. Additionally, it is important to note that a friend function is NOT a member of a class. Therefore:
.
or ->
operators, so it must have a pointer to the class as one of its parameters.For example:
// Class for storing times of day
class ToD {
friend bool set12(ToD &T, int lh, int bh, string ampm);
private:
int h, m;
bool PM;
public:
ToD(int lh, int bh, string ampm);
};
With the class defined this way, the function set12(ToD&,int,int,string)
has access to the private data members of ToD
, but nothing and nobody else does. So, the friend
construct allows us to grant access to specific functions, while keeping everyone else locked out.
So, in order to use the set12
function in main
we would simply do the following:
ToD time(10,30,"PM");
...
...
set12(time,11,00,"AM");
friend
function?In general, the friend
feature should be used sparingly. Before implementing a friend
function, you should first consider providing access via a member function. We want to practice information hiding, but if everyone is a friend
then nothing is hidden! If you find yourself having an abundance of friend
functions, it may be indicative of a problem with your design.
const
: Protecting your data in C++In its most basic usage const
can be used in front of a variable declaration to make the variable constant, i.e. to ensure that its initialized value would be the only value it ever had.
For example:
const int theGreatestClassAtUSNA = 2001;
In other contexts, as we'll see, const
is really more of a promise, one to which we'll be held, that an object won't be modified in a particular function. To understand the meaning (and importance) of const
in this sense, consider the following program, which uses a version of our good friend Point
:
Point.h | Point.cpp |
|
|
Now, this Point
class has a constructor and three member functions. Additionally, it has stand alone friend
functions readIn
and writeOut
. In the following code we try unsuccessfully to call these stand alone functions:
#include "Point.h"
#include <iostream>
using namespace std;
int main(){
Point P;
readIn(P);
writeOut(P);
readIn(P.reflectX()); // clearly won't work, it's like "cin >> (a - b)"
writeOut(P.reflectX()); // looks OK, but it isn't! Compiler doesn't know
// that it's any different than the previous statement
return 0;
}
When we try compiling this we get two error messages:
Error from call to readIn |
ex1.cpp:41: error: no matching function for call to 'readIn(Point)' /usr/include/unistd.h:425: note: candidates are: ssize_t readIn(int, void*, size_t) ex1.cpp:20: note: void readIn(Point&) |
Error from call to writeOut |
ex1.cpp:43: error: no matching function for call to 'writeOut(Point)' /usr/include/unistd.h:524: note: candidates are: ssize_t writeOut(int, const void*, size_t) ex1.cpp:27: note: void writeOut(Point&) |
This makes it look like the compiler cannot find the readIn and writeOut functions, but if you'll notice, the errors happen on the lines that take P.reflectX()
as the argument. The compiler is telling you thatreadIn(Point&)
and writeOut(Point&)
are candidates. The compiler sees them, but won't let you pass the result of P.reflectX() as a reference. Hmmm... hold on to that thought.
Now, the call to readIn
is clearly bad, since readIn
will try to change the value of the object returned by the function call P.reflectX()
. Well, this is an rvalue, it is an object that can appear on the right-hand side of an assignment, but not the left. It's like trying to x + 1 = a + b
. It's not a variable! The result of a function call or an expression evaluation is called a temporary. The object exists temporarily until the expression in which it occurs is evaluated, and then it dies. C++ does not let you modify temporaries, so it does not let you use them as lvalues. lvalues are things that can appear on the left hand side of an assignment.
Going back to our errors, and thinking about temporaries, does it make sense why the compiler is giving us this error? readIn
tries to modify the result of the function call, which is a temporary, and thus the expression is bad - it's an error. But, what about the call to writeOut
? It isn't going to try to modify the temporary returned by the function call P.reflectX()
, right? That's correct, but the compiler doesn't know it! You need to put the keyword const
in front of the parameter type in writeOut
's prototype and definition. This serves as a promise that the parameter won't get modified, and it's a promise the compiler believes when it compiles main.cpp
, because it will hold you to it when compiling Point.cpp
! Using const
, we can fix the whole thing!
Point.h | Point.cpp |
|
|
#include "Point.h"
#include <iostream>
using namespace std;
int main() {
Point P;
readIn(P);
writeOut(P);
writeOut(P.reflectX()); // Now everyone's happy. True, "P.reflectX()" is a
// temporary and writeOut is pass-by-reference, but
// the parameter is declared const, which is a promise
// that writeOut won't modify it!
return 0;
}
const
You might argue that there's no reason for the writeOut
function to be declared a friend
of the class Point
, since it could use the public member functions getX
and getY
instead of accessing the private data members x
and y
. I would say that sounds like a good point! Let's make the change:
Point.h | Point.cpp |
|
|
Unfortunately, when we try to compile our main function with the new Point.h
/ Point.cpp
files we get the following error message:
In function `void writeOut(const Point &)': passing `const Point' as `this' argument of `double Point::getX()' discards qualifiers passing `const Point' as `this' argument of `double Point::getY()' discards qualifiers
What does it mean? Well, first off you need to know that inside a member function you do not only have access to data members and functions, you also have access to a variable named this
, which is simply a pointer to the object from which the member function was called. So if the following member function call is made: A.f()
, inside the definition of f
the variable this
would point to A
. On the other hand, if the same member function is called as B.f()
, then in that call the variable this
in f
would point to B
.
So, let's try to dissect the error message now. Inside the definition of writeOut
, we have the parameter P
of type const Point &
. We make the call P.getX()
, and the compiler complains that passing a const Point
(i.e. P
) as the this
argument of getX()
(i.e. as the object from which getX()
is called) discards qualifiers (i.e. discards the const
).
Bottom line: The compiler is complaining because it doesn't have any guarantee that the member functions getX()
and getY()
won't modify P
in the calls to them! Since P
is supposed to be const
in writeOut
, the compiler needs to be sure that P
doesn't get modified.
Now, since we know that getX
doesn't modify its "this " argument, i.e. the object from which it is called, we simply need to make the const
promise to the compiler. The syntax for doing this is to put a const
immediately following the parenthesized argument list to the member function. In this case:
double getX() const { return x; }
double getY() const { return y; }
Now we have promised the compiler that we won't modify the "this " argument, i.e. we won't change P
in the call P.getX()
. Our code compiles just fine with this change. To finish this example off, let's note that the member function reflectX
could also be defined as const
, since we clearly do not expect a call like A.reflectX()
to modify A
.
A lot of object creation (and destruction) goes on "behind the scenes" in C++. The classic way to get a feeling for this is to write a program where the constructors and the destructor print out a message, so you can track when objects are created and destroyed. In the following program, see if you can determine how many Point
objects are actually created:
main.cpp | point.h | point.cpp |
|
|
|
It turns out that 7 different Point
objects are created by this program:
Four of these seven are easy to see: Points P
and Q
are created with default constructors when the variables are defined, and the two calls to midpoint
each create the point that they return with a call to the 2-argument constructor. Where do the three extra points come from? Well, notice that they are all created with the copy constructor. They come from the pass-by-value calls to midpoint
and writeOut
! All of this extra work can be avoided, however, by using pass by reference (with const
) instead!
main.cpp | point.h | point.cpp |
|
|
|
Now when we compile and run this version, those three "extra" points never get created:
The moral of this story? Pass-by-value is great for built-in types. For user defined types, however, it involves some overhead - all those calls to the copy constructor. Use pass-by-const
-reference instead.
Things really get ugly with temporary objects and copy constructors and all those other things when we have classes with data members. Here's an example illustrating why. Consider the following program:
main.cpp | vect.h | vect.cpp |
|
|
|
Everything looks perfectly reasonable, doesn't it? But look what happens when you compile and run this program!
Not exactly what I expected! What happened to the val
components of my Vect
? Well, look at main(): the call sum(V)
results in a copy of V
being sent to sum
, since we have pass-by-value. Since we have not definined our own copy constructor, the compiler supplies its own, which simply copies each data member individually. When the pointer V.val
is copied to A.val
, no new memory is allocated: A.val
simply points to the same array. Now, when sum
is finished, A
's destructor gets called, and it deletes the array A.val
. But this is the same array - physically the same array - as that pointed to by V.val
. Thus, we have no guarantee that our array elements won't get replaced by random garbage, and indeed in this case that is precisely what happened.
There are two ways out of this problem: 1) simply avoid pass-by-value and use pass by reference instead, or 2) define a copy constructor that makes a deep copy of Vect
, including allocating new arrays.
The easiest rule to making the headaches of this section go away is to simply abandon pass-by-value for user defined objects - or at least for objects with dynamic memory. By using pass by reference and const
, the resulting code is every bit as natural as if copy constructors are properly implemented, and a lot more efficient. In fact, other languages, like Java, don't even have pass-by-value for user defined objects. One good idea is to define a copy constructor with an empty body, i.e. a copy constructor that does nothing, in the private
section of your class definition. This actually disables pass-by-value for users of your class. That way they couldn't make this mistake if they wanted to.
The one thing you have to worry about, however, is correctly overloading an assignment operator for a class that has dynamically allocated data members. Using the above example:
Vect A(3);
...
...// some code here
...
if(true) {
Vect B(3);
B = A;
} // B goes out of scope and is destroyed, which also destroys A
// because the assignment operator set B's pointer to the same
// memory as A's pointer
You see, assignment has the same difficulty. You must either disable it by defining the operator as private:
private:
Vect& operator=(const Vect &A) { }
or you must define it to work properly:
Vect& operator=(const Vect &A) {
if (A != this) { // Sounds weird, but we want a statement like X = X to do nothing
delete [] val; // kill old array
dim = A.dim; // set new dimension
val = new double[A.dim]; // create new array
for(int i = 0; i < dim; i++) // copy contents of A into new array
val[i] = A.val[i];
}
}
If you think about your financial simulator, you should remember that you never had any reason to assign, for example, one Bank
object to equal another. Same for Expenses
and Income
objects. For many classes simply prohibiting pass-by-value and assignment make a lot of sense.