Previous Lecture | Next Lecture | Exercises | Top Level

References, Overloading, Constructors and Operators

Contents


Next Section | Contents

References vs. Copies

Today, I would like to start with a fairly general discussion of references and copies.

When I want to use an object provided by someone else, I have two choices: I can either request a copy of the object, or I can request a description on how to access the original object, called a reference.

Handing out a copy protects the original. It is therefore a very safe procedure. On the other hand, if I want to modify the original, I have to somehow merge the copy back into the original, which can be a very complicated procedure.

References makes changing the original easy. So easy in fact, that you have to worry about several people changing your object at the same time. References are inherently dangerous, and you have to take measures to protect your original.

Let's see this in action. We continue with last lecture's complex number example, and define a function that will multiply two complex numbers. Let's write this in C, where references are implemented as pointers:

struct complex {double x,y; };

void mult(struct complex op1, struct complex op2, struct complex* result)
{
  result->x = op1.x * op2.x - op1.y * op2.y;
  result->y = op1.x * op2.y + op1.y * op2.x;
}
As you can see, we are passing 3 parameters to the mult() function. op1 and op2 are passed by value, meaning that the function receives a copy of the original structures. result is a reference, passed as a pointer to a struct complex.

Let's use this function in a main program:

main()
{
  struct complex a = {1.0, 3.0};

  mult(a,a,&a);

  printf("a*a = %e + %e*i\n", a.x, a.y);
}
The output will be:
a*a = -8.000000e+00 + 6.000000e+00*i
Now let's suppose that we want to save the cost incurred in copying the two complex numbers, and use references everywhere. The program would look like this:

#include <stdio.h>

struct complex {double x,y; };

void mult(struct complex *op1, struct complex *op2, struct complex *result)
{
  result->x = op1->x * op2->x - op1->y * op2->y;
  result->y = op1->x * op2->y + op1->y * op2->x;
}

main()
{
  struct complex a = {1.0, 3.0};
  mult(&a,&a,&a);
  printf("a*a = %e + %e*i\n", a.x, a.y);
}
The result is not what we want:

a*a = -8.000000e+00 + -4.800000e+01*i
The reason is that in the first line of the mult() function, we are modifying our input data before we are finished using it. The dangerous thing is that the provider of this mult() function probably didn't anticipate the slightly unnatural usage in the main program (exercise: fix the mult() function without changing the call interface).

In C++, references are a distinct entity. You no longer need to use pointers to implement references. This has several advantages:

This is how our mult() function transforms itself into C++:

mult.c Mult.C

Note that within the code, you can no longer distinguish what is a reference and what is a copy. Therefore, it makes sense to use the following convention:


Previous Idiom | Next Idiom | About Idioms
If a function wants to return a value, then use return to return it as a function value. Always pass parameters as copies. If a function wants to return several values, consider grouping those values into a structure (that can be returned), or splitting the function up into several small functions that each return part of the requested values. Note that functions that want to modify a value should be implemented as member functions of that value.
In essence, the above examples are bad code. Do as I say, not as I do...
Previous Section | Next Section | Contents

Overloading

Now, let's add scalar multiplication to our complex arithmetic package. In C, we would have to invent a new function name, since mult() is already taken. But scalar multiplication is still a multiplication. It just uses different argument types. C++ is smart enough to know the difference, so the following is legal C++ code:

overload.C


Previous Section | Next Section | Contents

Basic Constructors

Let's review how variables get created, where they can live and who can see them:

variables.C

Static variables
are declared outside of any function block. They get created when the program starts and die when the program dies. They are visible by everyone within the same file. By omitting the static keyword, one can make the variable visible to everyone in the program (a global variable).

Previous Idiom | Next Idiom | About Idioms
Do not use global variables. Keep the internal representation of your data hidden from the users of your data. Otherwise, you are committed to maintaining the same datastructure forever. Use functions to access your data.
Automatic variables
are declared inside function blocks. They get allocated on the stack when the function is called. They are only visible inside the function block.
Dynamic variables
are allocated with the special new statement. You should use pointers to keep track of them, and delete them when you don't need them anymore.
Temporaries
are created whenever the compiler feels like it, usually, when evaluating complex arithmetic expressions or when processing function calls.
Whenever a variable gets created, it is initialized to 0. Whenever the compiler needs to make a copy of a variable, it is copied element by element. For complex classes, this may not be good enough and we need more control on how variables get treated. For example:

For this purpose, C++ uses constructors. Let's look at a few simple ones using our complex class from last lecture. Note how we can overload the constructor.
complex.h
complex.h
complex.C
complex.C
main.C
main.C
Note that assignments themselves do not need the copy constructor, since we aren't constructing anything at that point.


Previous Section | Contents

Operators

Now we have the tools to understand one of the neatest constructions in C++: operators. In standard C (and in most other programming languages), we have to use often clumsy function calls to express operations that can be written quite naturally with operators. Instead of relying on (English) function names we can rely on the (internationally) agreed meaning of operators. So, + would always mean the addition or gathering of stuff. += would imply some sort of accumulation, * the composition of stuff, -> the following or resolving of some sort of link, etc...


Previous Idiom | Next Idiom | About Idioms
Overloading the standard meaning of operators is a very powerfull tool to make programs simpler and shorter. Avoid abusing this feature by keeping to the natural meaning of the operator:

+ - * / etc...
Use for math and math-like operations;
== != < <= > >=
Use for comparing things;
[]
Use for indexing;
()
Use only on types with one single valid operation;
->
Use for resolving references ("smart" pointers);
It is a good sign if you are actually using the operator in it's original meaning inside the implementation of the operator.

Note that overloading does not change the syntactic properties of the operator. Binding and precedence remain unchanged.


Click here to look at a directory containing code files of a program to calculate eigenvalues using a novel algorithm. Have a look at the complex vector class and the complex matrix class definitions. Observe how simple a fairly complicated procedure looks like:

void iteration(...) {...}

At this point, I do not expect you to understand everything in those files, but the feeling should get through. As we go along in this course, more and more pieces will fall together.


Previous Lecture | Next Lecture | Exercises | Contents
Christian Goetze