This exercise deals with a standard C++ class.
The C++ codes in the course examples directory make ample use of the "string" class (try grep string ~p5720/examples/*.cc). Begin this exercise by copying the code in c++strings.cc in the class examples directory to your working directory. Look at the source code, then compile and run it. Note that the code defines variables which include a character (char c), a character array (char d[]), character pointers initialized to old C-style strings (char *p,*q) and finally C++ string objects (string s,t,u). In the code you will see an incredibly convenient notation for concatenating two strings:
u = s + t;
Here the "+" operator has been messed with (the technical term is
"overloaded" -- more on this later) so that it means concatenation
between the thing that precedes it ("s") with the thing that
follows it ("t"). This overloaded version of "+" works in this
way only if it operates on at least one string, that is, in
the expression "a+b" at least one of a or b must be a string.
If one is not a string, you'll want it to be something sensible
in the context of string concatenation (e.g., a char, char *, or
char []).
Given the behavior of "+" just described, guess which of the following will work:
u = "hi " + "there!";
u = q + p;
u = s + q;
Try it in your code. Also try "u=s-t;" (Does it make sense to
give the minus operator
meaning for strings? Do not be surprise if your answer is confirmed
by the compiler!) You should also check out conditional operators, e.g., "u==s"
and "u!=s".
This problems demonstrates the use of class member functions.
Unlike the "char" data types, "string" is a class. There are "methods" or "member functions" associated with a class, and for the string class these can do useful things. For example, try finding the number of characters in the string s with
cout << " the length of string s is " << s.length();Note: the syntax s.length() calls the string member function length() and has it operate on s. Also try
string v = "my name";
v.replace(0,2,"your");
to see the effect of the member function "replace". (There are quite
a few other string member functions....)
Finally, why is it important to know about C-style strings anymore? Here is the answer: Some C++ functions like the open() member function of the ifstream or ofstream class [see c++fileio.cc in the course examples directory]), and all old C functions (like the ascii to integer converter atoi()).
Modify your code by #including <cstdlib> at the top of the source file, and try the "atoi(w)" for cases in which "w" is a C-style string set to "1234", "w" is a (C++) string, also set to "1234", and "w" is a string but is replaced by "w.c_str()" when used as an argument to "atoi()".
Here we consider a class of our own design. It will allow us to work easily with (2D) vectors, something which can be very useful in physics.
In a file called "vec2d.cc", define your own class called "vec2d". Your starting point may be this code (in examples/c++class.cc):
#include <iostream>
class vec2d // begin class definition
{
public:
// members
double x,y;
// constructors:
vec2d() { x = y = 0; } // if no arguments set members to 0.
vec2d(double X, double Y) { x = X; y = Y; } // initialize using args.
// member funcs:
double length() { return sqrt(x*x + y*y); }
}; // end class definition
int main()
{
vec2d p;
vec2d q(2,2);
cout << " p: " << p.x << "," << p.y << endl;
cout << " q: " << q.x << "," << q.y << endl;
cout << " length of q: " << q.length() << endl;
}
Compile and run this code. Modify it so that
in main() you define vec2d variables "r" and "s" initialized to (3,4)
in two ways:
vec2d r; r.x = 3; r.y = 4;and
vec2d s(3,4);Notice that because r has no arguments, the statement "vec2d r;" invokes constructor vec2d(), hence r's elements get set to (0,0). Similarly "s" defined as above calls the form of constructor with two arguments.
C++ offers another way of initializing the elements of a class. For example, the 2-argument constructor could be written as
vec2d(double X, double Y) : x(X), y(Y) { }
Replace the original 2-argument constructor with the above code.
Next, modify the no-arg constructor using this same syntax,
"vec2d() :...{}".
Now, in main(), try initializing a new vec2d as follows:
vec2d t(6);
This won't work. Why?
Create a third constructor which takes only one argument. Have it
set the x component to the value of the argument and the y component
to zero.
C++ gurus would call this a "struct[ure]", not a "class" since everything in a structure is "public" by default. To explore the differences between public and private class members, move the containing the "public:" statment to the line just below the declaration of members x and y. After you do this, find out whether you can use the syntax p.x in your main() function.
This exercise is an introduction to operator overloading.
Start again with the original c++class.cc code that you used in the previous exercise, copying it to a file "vec2dol.cc". Now, in main(), try to set a vector r to have the same values as q, using the syntax
vec2d u = q;
Does this work? By default, the assign operator knows how to copy the
members of one class object to an object of the same class. You might
be surprised that we even bring up this point, afterall, that is what
"=" is supposed to do. However, there are cases where the assign
operator "=" might not do what you expect or want, particularly if a
class member is a pointer to some dynamically allocated array. The
default equal operator merely copies the address of the array (the
pointer's values is this address), whereas you might want it to copy
the array data itself to a new address. This can be fixed, by
"overloading", i.e., redefining, the operator "=" to suit your own
needs. But in our case, with vec2d, the C++ default version of "="
does what we want. According to B. Stroustrup, this is just an
"historical accident" in the design of C++. In a "perfect world" we
might have been forced to define the "=" operator from scratch. In
reality, we lucked out.
So let's try something slightly more complicated, the "add" operator. Here we are not so lucky. In your main function, type
u = q + r;
We want this to mean "set members of u to the sum of the corresponding
members of q and r". Try it. Does it work? You should not have gotten
past the compiler which should have whined about not knowing how to deal
with "+", even though it is quite obvious what it means to us.
What to do? We can give the "+" operator special meaning by overloading it. Just below (and outside of) your vec2d class definition (but before your main() function), type
inline vec2d operator+(const vec2d& lhs, const vec2d& rhs)
{ vec2d sum(lhs.x+rhs.x, lhs.y+rhs.y); return sum; }
What does this mean? Effectively, the statement says treat the syntax
"q+r" as if you were calling some C++ function, let's call it
"add(q,r)", which returns the vector sum of its two arguments. With
the operator overload, the "add()" function's name is actually the
operator symbol "+", and its two arguments do not need to be in parentheses
on the right of the function name. Instead, the first argument in the
operator overload function definition ("lhs" in the above example)
is the thing that appears on the left-hand side of the operator (LHS, hence
the name "lhs" for the argument), while the second argument is the thing
that appears on the right-hand side (RHS).
A few incidental comments: The "inline" keyword tells the compiler that you don't want your code to call the "+" operator as a seperate function. Instead, you want the "+" operator to get put right in line with your own code wherever you use it. This is optional, and should not be necessary for a good C++ compiler. The second comment is about the data types used in the operator overload definition. Note that the return type is a vec2d class object. It could have been anything -- we might have used "int operator+(...)" and had the operator always return an integer value of, e.g., -1 (though it would make no sense to do this for the "+" operator!). We return a vec2d object because that is what we want out of a sum of two vec2d objects. Finally, notice that the two arguments of the operator overlaod definition ("lhs" and "rhs") are of type "const vec2d&". We could have used simply "vec2d", in which case the compiler would take the statement "q+r", make copies of both "q" and "r" and place the copies in the "+" operator-function's argument list. It turns out to be more efficient to pass a reference to the arguments instead of the copied values, so we do it this way. The "const" keyword then is essential here: You MUST tell the compiler if you plan to modify any arguments passed as references. If you change the values of "q" or "r" in the operator overload, then do not use the "const" keyword. In our case we DO NOT wish to change the value of "lhs" or "rhs" via references to them, so we MUST put in the "const" keyword. If you do not follow this rule, then the compiler may whine irritably at you when you use multiple operator calls in a single statement like "q+(s+t)". Trust me. In the statement "q+(s+t)", the compiler makes a temporary storage place for the result "s+t", before adding it to "q". The value of "s+t" is effectively of type "const vec2d" since the compiler knows that once calculated, "s+t" is not going to change. Because the compiler CAN'T turn a "const vec2d&" type (something which cannot be modified) into a "vec2d&" type (something which can be modified), but visa versa is Ok, use the "const" keyword just in case the "+" operator has to deal with a reference to something which can't be modified.
Test to see that the overloaded operator "+" does what you expect. Also try something really nasty Modify your code so that "s+q" actually returns the vector difference of "s" and "q". Will this work? (This is an example of something horrible you can do in C++ to really confuse your collaborators.)
Next, let's get a little fancier by overloading the "+=" operator, as in
u += q;
Here we want to set up the "+=" operator so that "u+=q"
vector-adds "q" and "u", and puts the result into
"u". Hence, "u+=q" is shorthand for "u=u+q".
Now, let's work INSIDE the vec2d class definition e.g., just before or after the definition of member function "length()"). Try
vec2d& operator+=(const vec2d& rhs)
{ x+=rhs.x, y+=rhs.y; return *this; }
The above syntax has the following meaning: The operator "+=" has two
arguments, one on the left and one on the right, but now we only need
to specify the one on the right. Inside of the vec2d class definition,
it is ASSUMED that the argument on the left is of type "vec2d". You
can reference its members using just the member names ("x" or "y").
The name of the vec2d object on the left of the "+=" is identified with
the special name "*this" ("this" is a pointer to the object).
Note in the above definition we return "*this", the object on the left of the operator, so that the syntax "v=(u+=w)" actually sets "v" to the new value of "u". (If we vow not to use the syntax "v=(u+=q)", we could return anything we want here, say an int of -2. But this will give your instructor the hives, so please don't do that!) Finally, we return not a value, but a reference to a vec2d object. This is for efficiency's sake only. If we hadn't used a reference, the operator "+=" would have to make a copy of "*this" to return. But the variable on the left of the operator "+=" already contains the result of "+=" so we don't bother copying it, we just give a reference to it.
To emphasize that we could have returned anything we wanted, try
s = u += q;
first, then modify your operator overload so that "s" gets set to
"q" and not the final "u" value. Maybe try
vec2d operator+=(const vec2d& rhs)
{ x+=rhs.x, y+=rhs.y; vec2d fake = rhs; return fake; }
and then check on the value of "s". (Note, we changed the return type
of "+=" from a "vec2d&" to just a "vec2d". This
means we can return a COPY of a vec2d object "fake" which is defined
locally inside the overload definition, but which does not exist
(i.e., cannot be refered to) outside the definition. It is easy to
manipulate the local variable "fake" and simple enough
to copy its value to the "outside world". Try other perverse manipulations
of "fake", for example, return "fake" set to (0,0) or some other fixed
vetor value, but in the end reset your "+=" overload to work
properly!
Finally, go back to your overload of "+". Instead of doing the vector addition "by hand" in the overload definition, we can use the overloaded "+=" operator to make the code simpler. Thus rewrite your "+" overload as
inline vec2d operator+(const vec2d& lhs, const vec2d& rhs)
{ vec2d sum = lhs; return sum += rhs; }
As a matter of good taste, prefer this style over code which
manipulates class members directly.
Here we consider operator overloading in more detail. The starting point should be your final code from the previous exercise.
Two-dimensional vectors can be adjusted by multiplication with a scalar. To implement scalar multiplication, write
vec2d& operator*=(double a) { x*=a; y*=a; return *this; }
WITHIN your vec2d class definition. Now things like
"s*=10" will rescale the components of "s" by a factor of ten.
Next, go just outside of the class definition in your code and type
inline vec2d operator*(const vec2d& v, double a)
{ vec2d p = v; return p *= a; }
Note that we have "inline"d the operator to remind the compiler not to
make a separate operator function "*" that gets called elsewhere in
your program. Instead, it is a reminder that the compiler
should optimize and insert the code directly
in your program where needed.
Recall that because this operator definition appears outside of the class definition, you must specify two arguments explicitly, one corresponding to the variable on the LHS of the operator "*" and the other to the thing on the RHS. We could have put the operator overload inside of the class definition, in which case we would need only to specify one argument for the type-double argument on the right of the of the "*". The LHS in this case would have been "*this", implicitly understood to be of type vec2d, as in the "*=" overload above.
Now try
u = s * 10;
and
u = 10 * s;
Do these both work? The problem with the second one is that we have
not yet given the compiler a clue as to what to do with "a double
times a vec2d", only the other way around. Thus, add into your code
vec2d operator*(double a, const vec2d& v) { vec2d s = v; return v *= a; }
and try "10*s" again. Note that here the LHS of the operator is
a double, not a vec2d object; there is no way we could have
included this overload inside of the class definition since there
all LHS's are assumed to be vec2d objects.
While we're at it, let's overload the "*" operator for the case where we have a vec2d object multiplying another vec2d object. What do we want this to mean? One possibility is to create a third vec3d object with its members just the product of the corresponding members of the multiplicands. But this is not a very common thing to do the context of vector arithmetic. Instead let's make "*" mean the inner, or dot, product. Outside of the class definition, right along with your other "*" overloads, put
inline double operator*(const vec2d& u, const vec2d& v)
{ return u.x*v.x + u.y*v.y; }
Then try it out:
cout << q * r << endl;
This brings up another point. What if we were to "cout<<q", not
just its components or a type-double dot product with another vector?
Of course it won't work -- until we give the compiler explicit directions
with what to do for cout taking in a vec2d object.
Put this
code in after your class definition:
std::ostream& operator<< (std::ostream& s, const vec2d& a) {
return s << "(" << a.x << "," << a.y << ")";
}
This looks for occurrences of (e.g.) cout<<q and does the
right thing for an ostream class object (cout) and a vec2d object
under the influence of the << operator.
Now try cout<<q in your main() function.
Incidentally, the "std::" means to use the C++ standard library version of cout. It is just good form; it's not necessary here, especially if you had
using namespace std;
statement at the top of your code.