More About Classes
Friends and Members
A motivating example
Suppose we wanted to write a function to compare two Fraction objects.
An intuitive function call might be the following:
Fraction f1(1,2);
Fraction f2(2,4);
if ( Equals(f1, f2) ) // compare two fraction objects
cout << "The fractions are equal";
To achieve this call, we would need to build a function with the following
prototype:
bool Equals(Fraction x, Fraction y);
Notice that this would not be a member function of class
Fraction. The sample call did not use the dot-operator. Here's a
possible definition of the function:
bool Equals(Fraction x, Fraction y)
{
if ((x.GetNumerator() * y.GetDenominator()) == (y.GetNumerator() * x.GetDenominator()) )
return true;
else
return false;
}
Note that this algorithm results from finding a common denominator
(multiply the two denominators together), adjusting the numerators, then
comparing the numerators (integers). Also notice that this function calls
upon the accessor functions from the Fraction class --
GetNumerator() and GetDenominator() -- because it is not
a member function. It it outside the Fraction class.
Analyzing the situation
Because this function is performing an operation on two Fraction
objects, it is probably being written by the author of the Fraction class.
As such, it would be easier (and more efficient in run time) to write it
this way:
bool Equals(Fraction x, Fraction y)
{
if (x.numerator * y.denominator == y.numerator * x.denominator )
return true;
else
return false;
}
However, because this is an outside function, it does not have access to
private data, so this would be illegal, as is.
An alternative -- the keyword friend
- The keyword friend allows a class to grant full access
to an outside entity
- By "full access", we mean access to all the class' members, including
the private section
- An outside entity can be a function, or even another class (we'll
focus on functions for now)
- To grant friend status, declaration of the "friend" is made
inside the class definition block, with the keyword friend in
front of it
- A friend is neither public nor private, because by definition it is
not a member of the class. Just a friend. So it does not matter
where in the block it is placed.
- A friend function to a class will have full access to the
private members of the class. So, for example, the second definition of
Equals() above would be legal
- Here's a version of the Fraction
class with some friend functions
- This example contains the Equals() function given above
- This example also defines an Add() function, as a friend,
for adding two Fractions together and returning a result
- Includes a sample driver program that makes test calls to
Equals() and Add()
Member function instead of friend
- When a funtion works on two objects, it's often convenient to pass
both as parameters and make it a friend
- Another option is to use a member function -- but one of the objects
must be the calling object
- Example: The Equals() function could have been set up as a
member function, which would mean this kind of call:
if ( f1.Equals(f2) )
cout << "The fractions are equal";
This would be a member function. One object is the calling object.
The other object is passed as a parameter. This would be the
corresponding definition. (Compare it to the friend version).
bool Fraction::Equals(Fraction f)
{
if (numerator * f.denominator == f.numerator * denominator )
return true;
else
return false;
}
- Here's the Fraction class with
Equals() and Add() as member functions
- Includes a sample driver program that makes test calls to
Equals() and Add()
- Different programmers may have different preferences. Here's a
comparison of the calls, side-by-side:
f3 = f1.Add(f2); // call to member function version
f3 = Add(f1, f2); // call to non-member function version
- Also note that the member function version is not necessarily
equivalent to the friend function:
Fraction Add(Fraction f); // member
friend Fraction Add(Fraction f1, Fraction f2); // friend
- Notice that the parameters are pass-by-value, so copies of f1 and f2
are used in the friend function. Changes will not affect the
original objects.
- In the member function, the calling object could be changed by the
function.
- We'll see how to adjust for this shortly
Conversion Constructors
Recall how some of the built-in types allow automatic type
conversions, like this:
int x = 5;
double y = 4.5, z = 1.2;
y = x; // legal, via automatic conversion
z = x + y; // legal, using automatic conversion
Automatic type conversions can also be set up for classes, via a
conversion constructor
- A conversion constructor is a constructor with one parameter
- Constructors can be invoked explicitly to create Fraction objects.
Treat the constructor call as if it is returning an object of the
constructed type. The conversion constructor will be invoked
automatically when type conversion is needed:
Fraction f1, f2; // create simple fraction objects
f1 = Fraction(4); // explicit call to constructor. Fraction 4/1 is
// created and assigned to f1
f2 = 10; // implicit call to conversion constructor
// equivalent to: f2 = Fraction(10);
f1 = Add(f2, 5); // uses conversion constructor to turn 5 into 5/1
- In our previous Fraction class examples, the constructor with
two parameters has an optional parameter, so it counts as a
constructor with one parameter as well. (And is therefore a conversion
constructor):
Fraction(int n, int d = 1);
- In the event that a constructor with a single parameter should
not be used for type conversions, this feature can be supressed
by using the keyword explicit on the constructor declaration.
(This means that only explicit calls can be made)
explicit Fraction(double d); // will NOT be used for automatic conversions
- This version of class
Fraction illustrates usage of the conversion constructor in
sample calls in the driver program
Using const in classes
Remember that const can be used in a variety of places. And
everywhere it is applied in code, it:
- Expresses an intent on the part of the programmer, which the compiler
enforces (i.e. something is not allowed to change, within some scope)
- Expresses the intent more clearly to a user (in this case,
another portion of code -- i.e. maybe another programmer)
- Affects how certain items can be used.
- Sometimes this is the difference between using L-values vs. R-values
in calls to functions
- Sometimes this affects what other items can be used (whether they are
also const or not
)
Revisiting const reference parameters
- Pass-by-value vs. Pass-by-reference works the same on objects as it
does with built-in types
- If an object is passed by value, a copy is made of the object. Any
R-value can be sent on the call
- If an object is passed by reference (without const), no copy
is made, and only an L-value can be sent on the call
- Objects can be passed by const reference, as well. This way,
no copy is made (less overhead), but the object cannot be changed through
the reference. Since objects are sometimes large, this is often
desirable
- This example used pass-by-value parameters:
friend Fraction Add(Fraction f1, Fraction f2);
We definitely don't want to change the original fractions that were sent
in. But to save overhead, we could use const reference parameters:
friend Fraction Add(const Fraction& f1, const Fraction& f2);
Since the parameters are const, R-values can be sent in
(just like with pass-by-value).
const Member Functions
Declaring const objects
- Declaring primitive type variables as const is easy. Remember
that they must be initialized on the same line:
const int SIZE = 10;
const double PI = 3.1415;
- Objects can also be declared as const. The constructor will
always run to intialize the object, but after that, the object's state
(i.e. internal data) cannot be changed
const Fraction ZERO; // this fraction is fixed at 0/1
const Fraction FIXED(3,4); // this fraction is fixed at 3/4
- To ensure that a const object cannot be changed, the
compiler enforced the following rule:
- A const object may only call const member
functions
- So, using the Fraction class example above with const member
functions, the following calls are legal:
FIXED.Show(); // calling const functions
cout << FIXED.Evaluate();
int n = ZERO.GetNumerator();
int d = ZERO.GetDenominator();
The following calls would be illegal and would cause compiler
errors:
FIXED.SetValue(5,7);
ZERO.Input();
Note that in the original version of Fraction (with no const
member functions), ALL of these calls would result in compiler
errors, even if the function itself didn't change anything (like
Show).
- Examples:
const Member Data
- Member data of a class can also be declared const. This is a
little tricky, because of certain syntax rules.
- Remember, when a variable is declared with const in a normal
block of code, it must be initialized on the same line:
const int SIZE = 10;
- However, it is NOT legal to intialize the member data variables
on their declaration lines in a class definition block:
class Thing
{
public:
Thing(); // constructor -- intialize member data in here
// blah blah blah
private:
int x; // just declare here
int y = 0; // this would be ILLEGAL! cannot initialize here
const int Z = 10; // would also be ILLEGAL
};
- But a const declaration cannot be split up into a regular
code block. This attempt at a constructor definition would also
not work, if Z were const:
Thing::Thing()
{
x = 0;
y = 0;
Z = 10;
}
- Solution: When we have lines like this, which involve
declaring and initializing in one step and cannot be split up in normal
code, we handle it with a special section of a function called the
initialization list. Format:
returnType functionName(parameterList) : initialiation_list
{
// function body
}
- Simple class example that
illustrates the initialization of a const member data variable
Destructors
In addition to the special Constructor function, classes also have a special
function called a destructor.
Declaration:
The destructor looks like the default constructor (constructor with
no parameters), but with a ~ in front. Destructors cannot have parameters,
so there can only be one destructor for a class. Example: The
destructor for the Fraction class would be:
~Fraction();
Like the constructor, this function is called automatically (not explicitly)
-
A constructor is called automatically when an object is created.
-
A destructor is called automatically right before an object is deallocated
by the system (usually when it goes out of scope).
The destructor's typical job is to do any clean-up tasks (usually
involving memory allocation) that are needed, before an object is deallocated.
However, like a constructor, a destructor is a function and can do other
things, if desired.
Compile and run this example to see when
constructors and destructors are invoked.