 |
Department of Engineering |
 |
 |
C++ Notes for IIA Students
|
This document starts where 1B C++ teaching ends and illustrates
some extra C++ features which can prove useful when trying the IIA
Software project. Last year all the teams used pointers and references,
several of the teams used Exceptions and C++ strings, and they all developed classes (though none needed copy constructors). Below, a mixture of fragments and complete
programs is included. You'll find it
useful to compile and run the programs - they're designed to be run rather
than read.
|
|
Pointers and references are both used to deal with the same issue, and
they both use the & symbol. This causes confusion.
The general rule is to use references rather than pointers unless there's
no alternative, but you'll see pointers used in code so even if you
don't use them you'll need to understand them. Try running this program -
// Program 1
#include <iostream>
using namespace std;
int main()
{
int i=3;
int *pointer_to_integer; // this is a pointer
pointer_to_integer=&i;
cout << "i is stored at memory location " << &i << endl;
cout << "the value of i is " << *pointer_to_integer << endl;
}
Note that * and & in this context are complementary -
given a variable, using "&" finds its location, and if we
have a pointer to a variable (i.e. we know
the variable's location), then using "*" will give us the variable's
value.
Pointers are useful if we want to "call by reference" (rather than
"call by value"). Suppose
we want to write a function that will triple the value of a
given variable. We could try the following
// Program 2
#include <iostream>
using namespace std;
void triple(int i) {
i=i*3;
cout << "in triple, i is " << i << endl;
cout << "In triple, i is stored at memory location " << &i << endl;
}
int main()
{
int i=3;
cout << "In main, i is " << i << endl;
triple(i);
cout << "In main, i is now " << i << endl;
cout << "In main, i is stored at memory location " << &i << endl;
}
But it doesn't work as we wanted. The problem is that triple is never told where
main's i is stored so it can't change its value. The i in triple
is a different i which only exists in triple. It's stored in a
different place to main's i. That the 2 variables have the same name
is a coincidence. The only link between the 2 variables is that
when triple is called, triple's i gets its initial value from
main's i - i is being passed "by value".
If we give triple a pointer to i
so that triple knows the location of i, then it can change the value.
// Program 3
#include <iostream>
using namespace std;
// the next line tells "triple" to expect a pointer to an integer
void triple(int *i) { // this line has changed.
*i=*i*3; // this line has changed
}
int main()
{
int i=3;
cout << "i is " << i << endl;
triple(&i); // this line has changed too
cout << "i is now " << i << endl;
}
But this code is becoming cluttered with * and & symbols. C++ has a
way to do the same thing with less clutter.
// Program 4
#include <iostream>
using namespace std;
// the next line of code tells "triple" that i is being passed
// by reference.
void triple(int& i) {
i=i*3; // here i is an alias for main's i
}
int main()
{
int i=3;
cout << "i is " << i << endl;
triple(i);
cout << "i is now " << i << endl;
}
When the compiler sees
triple(int& i)
it knows that i is being passed "by reference" and does all the
extra work behind the scenes.
Programs 3 and 4 do the same thing.
References are preferable to pointers because they lead to tidier code and
they're safer. A line like
int *pointer_to_integer;
creates a pointer but doesn't point it to anything. If something like
*pointer_to_integer=3;
is done before the pointer is set to a useful memory location, disaster
ensues. In contrast, it's hard to create a 'dangling' reference.
For further information see
In the 1B course students use arrays of characters to contain text.
C++ has an alternative called strings.
The general rule is to use strings unless you have no
choice. Here's a simple example
// Program 5
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
s="hello";
s=s+" world";
cout << s << endl;
}
Note that the
string header file needs to be included. I think that this code
is as short and understandable as one could reasonably hope for. Note
that the string is "elastic" - it grows as required. Compare this code
with the old character-array method of C (though it's also legal in C++).
// Program 6
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
char s[10];
strcpy(s,"hello");
strcat(s," world");
cout << s << endl;
}
This code does the same as the first fragment does but is less readable
(strcat isn't a memorable name) and contains a bug - the s array
isn't elastic, it's only big enough to contain
10 characters. Here we're writing off the end of the array which could
have disastrous results. So C++'s more recent features are not only easier
to use than the old methods, they're safer too!
You can convert between C and C++ strings -
char cstring[10];
strcpy(cstring,"a test");
string a_str;
a_str=string(cstring);
strcpy(cstring, a_str.c_str());
You can also write to strings in the same way that you write to the
screen or a file. Try running this
// Program 7
#include <sstream>
#include <string>
#include <iostream>
using namespace std;
int main()
{
//stringstream s;
string t;
for (int i=1;i<20;i++){
stringstream s;
s << "I" << i;
s >> t;
cout << t << endl;
}
}
There's one trap to avoid when using C++ strings - you can use [ ... ] to access elements like you can with arrays, but you shouldn't access (for reading or writing) elements that don't yet exist. For example
#include <string>
#include <iostream>
using namespace std;
int main() {
string s;
s[0]='c';
cout<< s << endl;
}
might not display anything (and might crash) but
#include <string>
#include <iostream>
using namespace std;
int main() {
string s;
s="hello";
cout<< s << endl;
s[0]='c';
cout<< s << endl;
}
works ok.
C++ has a library of data structures (lists, vectors, etc) and about
40 algorithms to operate on those structures (sort, etc). In the
latest Deitel and Deitel C++ textbook, arrays and vectors are
introduced in the same chapter. vectors are no harder to use than
arrays, and offer several advantages. Here's a little example
#include <vector> // needed for vector
#include <algorithm> // needed for reverse
using namespace std;
int main() {
vector<int> v(3); // Declare a vector of 3 ints
v[0] = 7;
v[1] = v[0] + 3;
v[2] = v[0] + v[1];
reverse(v.begin(), v.end());
}
More examples are online. Once you've
managed to use one algorithm on a data structure you'll find that
other algorithms and data structures are similar to use.
Here's a simple extension of an earlier program using
the standard library's sort. A string of characters is being
sorted, but sorting a list of numbers or strings can be done in a very
similar way.
// Program 8
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
s="hello";
s=s+" world";
cout << s << endl;
sort(s.begin(),s.end());
cout << s << endl;
}
It's worth using C++'s off-the-shelf facilities whenever you can.
When a program is called from the Unix command line its name is sometimes
followed by strings. For example
g++ -v cabbage.cc
calls the g++ program with an argument and a filename. There needs
to be a way for g++ to
get hold of these arguments and filenames. Also it's useful for g++
to be able to pass back to Unix a return value. There's a standard
method for this. The first function called when a C++ program is run is
always
int main(int argc, char *argv[])
argc is how many strings were on the command line. argv[0]
is the first string (which will be the program name) and the other strings
(if any) are argv[1], etc. When main returns an integer,
this is passed back to the calling process.
To see how this works in practise, compile the following to produce a program called test1
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
cout << "The command line strings are -\n";
for (int i=0;i<argc;i++)
cout << argv[i] << endl;
return argc;
}
Typing
./test1 -v foo
echo $?
should print out the strings, then print "3" - the value
returned to the command line process.
The argument strings arrive into the program as character arrays. If you
prefer dealing with C++ strings you can use the Standard Library to
convert them into a vector (called args in this example) of C++ strings, using
some convenient constructors
#include <vector>
#include <string>
using namespace std;
int main(int argc, char* argv[]) {
vector<string> args (argv, argv+argc);
}
When trying to understand source code written by others, it helps to look at the include files first to identify the main classes and data structures. Don't try to understand each line of each function - with luck, the function names and comments will tell you enough. Try first to follow the code in top-down fashion, starting at the main routine.
In unix, grep is a useful command when you have many files.
If you're looking for where a function (add_device for example)
is mentioned, you can do
grep add_device *.h *.cc
to print out all the lines in the source code that mention add_device
The code may use C++ features you've not seen before. For example, the
following construction isn't often taught to first years.
sig = (target == low) ? 1 : 2;
This uses a "? ... :" construction. The RHS has the value
1 or 2 depending on whether target == low is true. It's
equivalent to
if (target == low)
sig=1;
else
sig=2;
There's also a commonly used ploy in include files. When you write big programs you're likely to have several include files.
Suppose you have 2 include files like this
// this is myfile.h
int globali;
and
// this is myfile2.h
#include "myfile.h"
int globalj;
Now suppose that in your main file you have
#include "myfile.h"
#include "myfile2.h"
The pre-processor (the first stage of compilation) expands #include directives etc - it's
a sort of filter whose output is passed on to the next compilation stage Typing
g++ -E main.cc
will just run the pre-processor, letting you see what the compiler receives. In this case the pre-processor's output will be
int globali;
int globali;
int globalj;
which is a bug - you can't create the same variable twice. There's a standard
way to guard against double inclusion. If the files are changed to become
// this is myfile.h
#ifndef MYFILE_H
#define MYFILE_H
int globali;
#endif
and
// this is mydfile2.h
#ifndef MYFILE2_H
#define MYFILE2_H
#include "myfile.h"
int globalj;
#endif
then when main is processed, the preprocessor will reach #ifndef MYFILE_H
(ifndef means "if not defined") so MYFILE_H will be defined as a value inside the preprocessor and int globali; will be let through. Then it will
reach #ifndef MYFILE2_H and define MYFILE2_H. At this stage it
will read myfile.h again but this time MYFILE_H is already defined,
so the contents of MYFILE_H will be ignored, which solves the problem. For each source file, each include file will be read at most once.
For this method to work, each include file must have a different "guard variable" name. By convention the name used is the filename in upper case with _H instead of .h.
In C, return values of calls had to be checked for error values - which could
double the code size. C++ exceptions are an alternative to traditional
techniques. They're not always better than using return values, and can be
over-used. Three
keywords are involved
- try - specifies an area of code where exceptions will operate
- catch - deals with the exceptional situation produced in the previous try clause.
- throw - causes an exceptional situation
When an exception is 'thrown' it will be 'caught' by the local "catch" clause if one exists, otherwise it will be passed up through the call hierarchy until a suitable "catch" clause is found. The default response to an exception is to terminate. The example below demonstrates some features. Note that
- A program can throw whatever's thought useful - a number, a message
or an object. There are standard exception objects or you can use
one of your own.
- "catch" routines are like other C++ routines in that there can be several
of them taking different arguments
- Once the exception has been caught and dealt with, the program
continues from just after the "catch" routine. If your "try" region
surrounds the whole program this means that execution will end, but you
can put "try{...}" around a localised region so that execution continues, as
in the following example.
#include <iostream>
#include <string>
using namespace std;
class Ball {
public:
int number;
string message;
};
int main() {
for (int i=1;i<4;i++) {
try {
switch (i) {
case 1: throw 999 ;
case 2: throw "help!";
case 3: Ball *ball = new Ball;
ball->number =999;
ball->message="help!";
throw ball;
}
} // end of try
catch(int errornumber) {
cerr << "error number is " << errornumber << endl;
}
catch (const char* errormessage) {
cerr << "error message is " << errormessage << endl;
}
catch(Ball *b) {
cerr << "error " << b->number << " - " << b->message << endl;
}
} // end of for
}
If you have
int speed=3;
in a file outside of all functions, then this variable can be accessed from other files as long as the other files have
extern int speed;
speed is a global variable. Such variables are considered bad style (they're error-prone) though sometimes
they're hard to avoid. There's a complication if extern is used with const. If you have
const int speed=3;
the variable isn't visible from other files. Here's a table
showing some examples of how the contents of 2 files can interact.
| File 1 | File 2 | Outcome |
int i;
int main()
{
i=5;
}
|
int i;
|
Fails (multiple definition of 'i') because when the 2 compiled files are linked together they each have an 'i' that is visible to the other.
|
int i;
extern int i;
int main()
{
i=5;
}
|
int j;
|
Fails (undefined reference to 'i') because foo1.cc expects an 'i' variable to be available from another file.
|
static int i;
int main()
{
i=5;
}
|
int i; |
Compiles. File 1's 'i' is private
|
const int i=5;
int main()
{
;
}
|
int i; |
Compiles. File 1's 'i' is private (const vars are static by default) .
|
extern const int i=5;
int main()
{
;
}
|
int i; |
Fails (multiple definition of 'i') because when the 2 compiled files are linked together they each have an 'i' that is visible to the other. Why? Because if a variable is declared as extern and it's initialised, then memory for that variable will be allocated. So in this situation there's an 'i' in each file.
|
extern const int i=5;
int main()
{
;
}
|
extern const int i; |
Compiles. There's only one 'i' variable in the resulting program - the 'i' in file 2 refers to the 'i' in file 1. If the line in file 2 was extern const int i=2;, linking would fail
|
In C++ you can create ints and floats, etc, but you can also invent
more complicated types of things. For example, if your program
deals with people, you might want to create objects designed to store
information about people. Here's a simple example
class person {
public:
float height;
string name;
};
This piece of code doesn't create a person object, but makes it possible to
create one. Just as you can create an integer by doing "int i;"
so you can now create a person by doing
person p;
Once you've created a person you can then fill in the details. E.g.
p.height=1.73;
p.name="simon";
As well as having values like height, etc a person can also have actions.
For example
class person {
public:
float height;
string name;
void sayhello() { cout << "hello!\n"; };
};
gives each person an extra ability which can be called by doing
person p;
p.sayhello();
person is an example of a Class, and p is an object of type person. Whenever
an object is created, a special action (called a "constructor") is
run. If you don't write one yourself, a default one is called. The
constructor function has the same name as the class itself so if we want
to write our own we could say
class person {
public:
float height;
string name;
void sayhello() { cout << "hello!\n"; };
person() { sayhello(); cout << "I've just been created\n";};
};
So now,
person p;
person q;
would produce the output
hello!
I've just been created
hello!
I've just been created
This constructor takes no arguments. We could also provide a
constructor that takes one argument
person(string n) { name=n; sayhello();
cout << "I've just been created\n";};
which would give us the chance to name people as we create them by doing
something like
person p("eve");
When an object is destroyed, a destructor function is called. For
our object the destructor would be called ~person, but before we
can show it in action we need to set up a situation where people die.
// Program 9
#include <iostream>
#include <string>
using namespace std;
class person {
public:
float height;
string name;
void sayhello() { cout << "hello! I'm " << name << "\n"; };
person(string n) { name=n; sayhello(); cout << "I've just been created\n";};
person() { name="NOBODY"; cout << "I've just been created\n";};
~person() { cout << name << " is about to die\n";};
};
void testfunction() {
person p("adam");
}
int main() {
person q("eve");
testfunction();
}
This re-uses much of the earlier code (the original constructor now sets
name to NOBODY for safety's sake).
Here, a person is created in the main routine then testfunction is called.
In testfunction another person is created, but the lifetime of that person
is only as long as the lifetime of the testfunction routine. If you
compile and run this you'll get
hello! I'm eve
I've just been created
hello! I'm adam
I've just been created
adam is about to die
eve is about to die
It might be useful to know how many persons exist at any particular moment.
The variables like name and height are unique to each object but it's
possible to create a single variable that all the persons can access.
The syntax isn't simple, but the facility's useful. We can use this
variable as
a counter, adding 1 to it when a person is created and subtracting 1
when a person dies.
Here's the revised code -
// Program 10
#include <iostream>
#include <string>
using namespace std;
class person {
static int howmany; // all persons share this one variable
public:
float height;
string name;
void sayhello() { cout << "hello! I'm " << name << "\n"; };
person(string n) {
howmany++;
name=n;
sayhello();
cout << "I've just been created. There are now "
<< howmany << " of us.\n";
};
person() {
howmany++;
name="NOBODY";
cout << "I've just been created. There are now "
<< howmany << " of us.\n";
};
~person() {
howmany--;
cout << name << " is about to die, leaving "
<< howmany << " of us\n";};
};
void testfunction() {
person p("adam");
}
int person::howmany=0;
int main() {
person q("eve");
testfunction();
}
This may already look like quite a long and complicated program.
On the plus side
- Its use of classes is already more advanced than is required to cope with
the demands of the IIA Software project
- the longest routine is only 6 lines long
- the "top level"
code (in main and testfunction) hasn't required changing. You'll find
that much of the work in C++ programs involves developing the objects so
that the "top level" is uncluttered.
On the minus side, the counting functionality isn't finished - there
are ways of creating objects that we haven't taken into account. To see
this, add the following line to the end of main
person r=q;
If you compile and run this you get additional output
eve is about to die, leaving -1 of us
The death of the "eve" clone has been registered, but not the clone's creation.
When a new person is created by copying an existing one, it uses a different constructor
function called the "copy constructor". If you add the following
copy constructor to the person class, things will be better
person(const person&) {
howmany++; sayhello();
cout << "I've just been created. There are now "
<< howmany << " of us.\n";
}
- There's some duplicated code in the person-based code above which (following the 3F6 notes) is a bad thing. Try to factorise the code.
- Write a program with a function that takes a person as an
argument. First pass by value then pass by reference, noting how the
count changes.
- Sometimes it's useful to create "singleton" classes - classes where only
one object of that type can exist. Adapt the code so that person
becomes a singleton class.
- For further developments see Counting Objects in C++
by Scott Meyers.