C++ is an extension of the C programming language, the extensions are primarily to support object oriented programming. C dates from the early 70s, its original purpose was to re-write one program, the Unix operating system. Since then it has spread widely due to the spread of Unix and its own simplicity and portability. During the 70s it evolved fast acquiring type checking and other features. In the late 70s and early 80s it was more stable even though there was no standard until the mid 80s.
C++ was developed by Bjarne Stroustrup in the early 80s in AT&T's Bell Laboratories. It is an object oriented extension of C and most C programs can be compiled by C++ with no change. There is now a draft ANSI and ISO standard, it hasn't yet (1997) been finally approved but will be soon.
Stroustrup has said that many of the features of C++ have been influenced by ideas that originated with Simula-67. The other main factors affecting the design of the language were efficiency and availability. The efficiency was important because the only widely known object oriented language at the time was Smalltalk-80 which was thought to be too inefficient to use for production programs. This ``inefficient'' label also seemed to be associated with object oriented programming, so C++ has designed to allow object oriented techniques to be used for production programs with minimal overheads.
Another important goal was that the language should be quickly accepted by programmers. The way this was achieved was to make it a superset of C which would ease the transition to object oriented techniques for C programmers, they wouldn't feel it was completely alien. In addition, the way it was built on top of C made it quickly available on any machine that already had a C compiler.
Unfortunately there is a disadvantage to building on top of C -- you inherit all the deficiencies. There are many features of C that can make program development and debugging quite difficult. For example the simple conditional statement:
if( x = y ) {
...
The following is a random selection of some books that are not too bad, in most cases there will be some comment on, what I estimate, to be the intended audience. First, the definitive description of the language by its designer Bjarne Stroustrup, The C++ Programming Language, third edition [6]; it is a very comprehensive description of all features of the language and how to use them but it is not very easy as an introduction.
There are some books that are very introductory, The apprentice C++ programmer by Lee and Phillips [3] assumes no knowledge of programming at all. Similarly the book by Perry and Levin, An introduction to object-oriented design in C++ [5] introduces the idea of object oriented programming; it does eventually cover all aspects of the language. An introductory book but with, perhaps, more about C++ than about object oriented design, is C++, How to Program by Deitel and Deitel [1]. Another introduction that really emphasises the C++ language with not much about how to use it is in the Schaum series, it is called Programming with C++ by Hubbard [2].
C++ Primer by Lippman [4] is a fairly systematic coverage of C++, make sure to get the third edition as the earlier editions are much weaker on object oriented programming. There is also a short book by P. H. Winston [7] that has explanation and uses lots of examples in the text, good, if you like that sort of thing.
C++ is an object oriented language like Java, Eiffel and others. They all have classes, inheritance and subtyping, so you can design programs in very similar ways in all languages, the structures will be similar. However there are lots of differences:
C, upon which C++ is based, is an older procedural language this makes it different from newer languages like Java or Eiffel which were designed to be to object oriented without being an extension of an older, non object oriented, language. In addition to classes C++ has all the features of older languages such as: variables, expressions, subroutines (called functions), the normal assignment, conditional and loop statements and simple composite data types: arrays, records (structs) and pointers. The basic types are:
The following program illustrates some of its features:
// Example Program 1, print powers of 2 less than 10000
#include <iostream>
using namespace std;
const int LIMIT = 10000;
int main() {
int pot = 1;
while ( pot < LIMIT ) {
cout << pot << "\n";
pot = pot * 2;
}
}
#
are dealt with before real compilation starts.
The #include directive causes the named file to be textually
included in the source file before it is fed to the compiler. It
is used to provide a way of incorporating a lot of declarations.
The file iostream.h contains declarations about the standard
IO library, it defines, for example, the class type of
the object cout.
const
directive is a very similar to a
variable but it is a read-only variable, in this case:
const int LIMIT = 10000
// which cause
the rest of the line to be ignored
or are surrounded by /*...*/ and can span many lines.
All simple statements (eg. assignment, procedure call) are followed
by a semicolon ``;'', while loops and compound
statements {..} are not followed by semicolon.
main),
main(). The body
is enclosed in braces {...} and contains zero or more
declarations followed by statements.
int main(int argc, char *argv[])
int main()
int) followed
by a comma separated list of variables with optional initialisation. So to
declare one variable pot and initialise it:
int pot = 1;
(..) and a statement.
while ( expression ) statementexpression is evaluated and if true statement is executed, this is repeated until expression is false when the while finishes and statements after the while are executed. Above it was required to repeat 2 statements so they are enclosed in
{..} to make them a single compound statement.
<<
takes its right hand argument and prints it in a suitable
form on the screen. Strings can be printed too:
cout << "hello world\n";
\ are treated
specially: \n is a newline, \t is a tab, \b is
a backspace and \\ is a backslash.
= for the assignment operator. So:
pot = pot * 2; assigns twice pot to pot.
1 1 2 3 5 8 13 21 34 ...
This second example is a simple program to read a sequence of integers up to end of file, and then print the value of the number which was the largest.
#include <iostream>
#include <limits.h>
using namespace std;
int main() {
int num, count=0, max_so_far=INT_MIN;
while ( cin >> num ) {
if ( num > max_so_far ) max_so_far = num;
count = count + 1;
}
if ( count > 0 )
cout << "largest of = " << max_so_far << "\n";
else
cout << "no numbers input\n";
}
limits.h comes from C but like many other standard C headers is available in C++. In the new C++ standard there is supposed to be a generic class defining various ``limit'' values as consts to replace the C ones but not all implementations provide this part of the library yet.
Unfortunately in this one particular context, checking for
end of file of a C++ stream, it is probably necessary.
Consider the simple clear loop:
while(! cin.eof() ) {
cin >> num;
...
}
In C++ all input stream operations (», get() and others) have been designed to cope with this. If the read is successful they will store a value and return the bool value true. If they fail to read, because of end of file or errors, they return false.
So in this one context, although I think it is a bad principle, I use the side effect of doing input in the conditional expression of a while loop.
The condition expression is evaluated and if true stat1 is executed, if it is false stat2 after theif(expression)stat1elsestat2
else is evaluated.
else part.
This third example will be used to explain about characters and their input and output. The program simply copies all its standard input to the standard output:
#include <iostream>
using namespace std;
int main() {
char c;
while ( cin.get(c) ) cout.put(c);
}
The expression cin.get(c) reads the next character
from the standard input into the variable c. If
it succeeds it returns true and the
body of the loop is executed calling cout.put(c)
which writes the character to standard output.
Why use cin.get(c) instead of cin » c? The operator » works but it skips white-space characters, newline, tab, and space. So the program:
#include <iostream>
using namespace std;
int main() {
char c;
while ( cin >> c ) cout << c;
}
if compiled, run and given itself as the file to copy,
will produce:
rabbit(273)$ g++ copy2.cc
rabbit(274)$ a.out < copy2.cc
#include<iostream>intmain(){charc;while(cin>>c)cout<<c;}
This example shows the use of character constants. If a text file has lines longer than a certain width it can be hard to display or print it as sometimes the lines get truncated. The following program wrap reads its standard input and writes the text to its standard output. If a line is longer than LINELENGTH characters it ``folds'' it by inserting a newline character and continuing the rest of the characters of th input line on the next output line. The program can be used as a filter before printing or displaying files.
#include <iostream>
const int LINELENGTH = 30;
using namespace std;
int main() {
int pos = 0;
char c;
while ( cin.get(c) ) {
if (c == '\n') pos = 0;
else pos = pos + 1;
if (pos > LINELENGTH) {
cout << '\n'; pos = 1;
}
cout << c;
}
}
==,
and != for not equals, NB. this must not be confused with
= which is assignment. This causes many problems in programs
because = is legal in expressions but has completely different
effects.
'..' is a character constant (actually the integer value
of that character in the character set as chars are
really little ints), non-printing characters are
represented by \n, \b, \0, \t and \r for
linefeed, backspace, null, tab and carriage-return respectively.
Write a program to read the standard input and write each character to the standard output except upper case letters which must be converted to lower case.
Note: in order to convert the upper case letters you must be able to
determine if a character is between 'A' and 'Z' and if so output
the appropriate lower case letter. Conditions such as ==, !=,
<, >= or <= can be combined with && meaning ``and''
or || meaning ``or''. So if(a==10 && b>0) means: if a
equals 10 and b is greater than 0.
Since chars are small integers they can be used
in arithmetic expressions.
So c - 'A' produces 0 if c contains 'A', 2
if c holds 'C' etc. And 'a' + 3 is the letter 'd'.
This section introduces the definition and use of functions in C++. The following main program repeatedly reads two numbers, a real, and an integer, then calls the ``power'' function and then displays the result. The looping will stop when it fails to read the numbers successfully.
#include <iostream>
using namespace std;
extern double power(double, int);
int main() {
int pow;
double res, num;
while(cin >> num >> pow) {
res=power(num,pow);
cout << num << " to power " << pow << "=" << res << "\n";
}
}
Notice the extern declaration1of power as a function
returning a double (a double precision real value)
result and taking a double and an int
as arguments; externs can be read as saying: ``somewhere
else there is a function of
this type''.
This is necessary so the compiler knows the type when it
encounters the call (in this case power(num,pow)).
Then ``somewhere-else'', in a separate file or later in the same file,
there must be the full definition of the function:
double power(double n,int p) {
if(p < 0) return 0.0;
if(p == 0) return 1.0;
return n * power(n,p-1);
}
This definition of the function will not cope with negative
powers, it returns zero. The zeroth power of any number
is 1.0, for any other power:
Functions are defined with a header eg: double power(double n,int p)
(which declares the result type, here double, the name, power,
and the types and names of the arguments separated by commas)
then the body which contains local declarations and statements.
When a function is called eg: ..power(2.0,i).., the actual argument
expressions are evaluated and the values assigned to the corresponding
formal arguments, ie. n=2.0 and p=0 (i is 0 the first
time). Then the body is executed. Execution continues until either the end
of the body is reached or a return statement is encountered.
If the return has a expression it is returned as the value of the
function. All non-void functions must have a return with an
expression.
All functions have a result type but if a function is only to be used as a procedure then the word void should be used instead; void is not a type it means there is no result. For example putlots takes a character c and a count n and outputs the character n times but there is no result:
void putlots(char c, int n) {
int i=0;
while(i < n) { cout.put(c); i=i+1; }
}
If a function has no arguments the declaration and definition (not the call)
must have an empty argument list. eg:
extern void greeting();
int main() {
greeting();
...
}
void greeting() { cout << "hello world\n"; }
This differs from ANSI C where the reserved word void is used in
the argument list of the declaration and leaving the argument list
blank means something else and is a hang-over from pre-draft standard C.
The only way parameters are passed in C, and the primary way in C++, is by value; this means that the expression for the actual parameter is evaluated and its value is then copied to the formal parameter which is then a sort of temporary separate variable for the duration of the execution of the function. So:
void twice(int n) {
n = n * 2;
}
int main() {
int x = 10;
twice(x);
... // x is still 10
}
has no effect whatsoever on the variable x in the main
program; in the call twice(x) the actual argument expression
x is evaluated giving 10 which is copied to the
formal n, then n = n * 2 is executed which
changes the local formal variable n leaving x
untouched.
To get a function like twice to alter the value of a variable given as an actual argument C++ provides reference parameters.
void twice(int &n) {
n = n * 2;
}
int main() {
int x = 10;
twice(x);
... // now x contains 20
}
Notice that the declaration of the formal parameter
is int &n. When the function is called
the actual argument must be a variable and it is
not evaluated, instead when the function is executed
the formal parameter name, in this case n,
acts as an alias for the actual variable so
that all uses of n actually refer
to x (in this case). So the
expression
n = n * 2;has the same meaning and effect as:
x = x * 2;
This example is very simple, it is intended to
present some of the details of how to write
classes, it is not meant as an example
of a useful class in a real application.
The program reads numbers from standard input, cin,
until end of file, it accumulates a running total in an
Accumulator object
and at the end prints the mean. The requirements
for the ``running total'' class are:
(i) keep track of the sum,First the main program including the class definition, this is file mean.cc:
(ii) record the number of numbers making up the sum,
(iii) have a way to add in numbers, and
(iv) have a way to get the mean.
#include <iostream>
using namespace std;
class Accumulator {
private:
double total;
int count;
public:
Accumulator() { total = 0.0; count=0;}
void add_in(double n) {
total = total + n; count++;
}
int number() { return count; }
double mean() { return total/count; }
};
int main() {
Accumulator acc;
double x;
while(cin >> x) acc.add_in(x);
cout << "mean of " << acc.number()
<< " numbers is " << acc.mean() << "\n";
}
The following shows the program being compiled and
run using the GNU compiler; the standard input, cin,
is the terminal, to simulate ``end of file'' from
the keyboard type ``control-d'' was typed after the numbers
but it wasn't echoed:
rabbit(269)$ g++ mean.cc rabbit(270)$ a.out 1.1 2.2 3.3 4.4 5.5 mean of 5 numbers is 3.3 rabbit(271)$
Accumulator acc;
.'' followed by the member name. Member functions are
the same, so the call of mean in an object acc
is: acc.mean(x).
Inside a member function access to fields is direct and does not require
selection if they are the fields of the object on which
mean was invoked, eg:
double mean() { return total/count; }
acc.count in main is illegal.
All the fields after a public
label are accessible from outside by selection. This is used to enforce
data hiding: hidden data fields use: private, accessor
functions use: public.
Accumulator acc;
causes Accumulator() to be called inside
acc which sets total and count to 0.
The word ``constructor'' can be misleading, it is better to think of them as initialisers, the storage has already been allocated before they are invoked, and their job is just to set initial values.
If a program is large enough to be broken into
more than one file, and if the same class is to be
used in more than one of the files making up the program,
then the class declaration is usually put in a header
file (a ``.h'' file). Classes are nearly
always in their own separate header files.
It is necessary to do this because since the
class is a type declaration it must be available to the compiler
during the compilation of each file
that has code that needs to use it. To avoid
duplicating the definition, it is put in a header file
and then ``included'' into each file that wants to use it.
The #include causes textual substitution of the
definition during compilation, therefore header
files never need compiling separately, they are compiled
whenever a code file including then is compiled.
Putting the class in a header file makes it
easier to re-use in other programs.
Here Accumulator is in file accum.h:
class Accumulator {
private:
double total;
int count;
public:
Accumulator() {
...
};
Now the main program file looks like this:
#include <iostream>
#include "accum.h"
using namespace std;
int main() {
Accumulator acc;
double x;
while(cin >> x) acc.add_in(x);
cout << "mean of " << acc.number()
<< " numbers is " << acc.mean() << "\n";
}
To declare an array just follow the name by square brackets containing a constant expression, the element type is determined by the name at the front of the declaration, eg:
int i, a[10];
char line[132];
declares i to be an integer, a to be an array of 10 ints
and line to be an array of 132 chars. Notice that
the easiest way to read C++ declarations and definitions
is inside-out: ``[start with the variable name] line
is an array of 132, [to the front] chars''.
Access to an element of
an array is by indexing so: line[3] accesses the 4th element of the
array line, note that all arrays in C++ start at zero so the first element
of a is a[0] and the last is a[9]. C++ never checks the
index value of array accesses so line[201] is legal but will yield
some unknown value at runtime.
The following small program reads a line of characters from the terminal and then writes them out again:
#include <iostream>
using namespace std;
int main() {
char line[132], c;
int i, nxt=0;
do{ cin.get(c);
line[nxt] = c;
nxt++;
} while(c != '\n');
for(i=0;i<nxt;i++) cout.put(line[i]);
}
In addition to using an array of characters line this program also
introduces a do while loop, the increment operator ++ and the
for loop.
);,
the statements in the body must be enclosed in {..},
the terminating ``;'' is part of the syntax. The loop executes its
statement then if expression is true (non-zero) it repeats.
++, eg: i++ increments i, and
++line[3] increments the 4th element of line. There is
also a decrement operator: --, so x-- decrements x by 1.
where the exprfor(expr![]()
;expr![]()
;expr![]()
)statement
exprso ``![]()
;
while (expr![]()
) {
statement;
expr![]()
;
}
for(i=0;i<nxt;i++) stat''
sets i to 0 and while i
is less than nxt, executes stat, increments i,
re-checks i etc.
Write a program to read integers up to end of file and then print them out in reverse order.
This example shows how a queue (or buffer) of characters
can be written as a class.
Code using the queue only has access
to the functions: join(c) which puts a character at
the back of the queue, head() which returns whatever
character is at the front, leave() which discards
the character at the front and empty() which gives true
if the queue is empty. In other words the only view clients have
of a class object is via the operations on it and this is enforced
by the scope rules of C++.
If a class Queue has been defined then a program could use it
to declare a variable b. The following
program reads characters and buffers them upto a
newline ('\n') from the standard input
then it extracts and outputs them until the queue is empty:
#include <iostream>
using namespace std;
#include "queue.h"
main() {
Queue b;
char c;
do { cin.get(c);
b.join(c);
} while(c != '\n');
while(! b.empty()) {
cout.put(b.head());
b.leave();
}
}
The definition of the queue class is in queue.h. The declaration
Queue b declares a variable of type Queue and allocates
storage on the stack for the components of the buffer.
The queue is implemented by using a fixed length array of characters and 2 integer indices, one to point to the front and one for the back. When a character is added to the queue (with join) it is stored at the position indexed by back and then back is incremented. Since front and back start at the same position the head() character is in the position indexed by front. To discard a character from the head of the queue leave() just moves the front index on.
0
Sub-figure (i) a b c d added
0
Sub-figure (ii) e f g added a b departed
0
Sub-figure (iii) u at front and y at back
|
In order to determine if the queue is full or empty there is a count of the number of elements it contains. It is not essential because the relative positions of the front and back indices could be compared; it is just simpler to use count.
The whole class definition is in one file queue.h that was included into the calling program. Here are the contents of that file:
#include <assert.h>
const int SIZE=256;
class Queue {
private:
char mem[SIZE];
int front,back,count;
int succ(int x) {
if(x < SIZE-1) return x+1; else return 0;
}
public:
Queue() { count=front=back=0; }
bool empty() { return count==0; }
bool full() { return count==256; }
char head() {
assert(! empty());
return mem[front];
}
void leave() {
assert(! empty());
front=succ(front); count = count - 1;
}
void join(char c) {
assert( ! full());
mem[back] = c;
back=succ(back); count = count + 1;
}
};
If head is called but the queue is empty what should be
returned? Nothing, there is no possible answer, there is nothing there.
The queue has been ``abused'' by the client code. In a sense
the program has an internal logic error in the code that
attempted to retrieve an element when there wasn't one.
There are various ways to deal with this: try to return an
``error'' value, raise an exception
or (as here) crash the program. The assert macro is defined
in assert.h. When assert is executed it
evaluates its expression argument and if it is true nothing
happens, but if it is false then the program is stopped
with an message saying which assertion has failed. Here is a test:
#include <iostream>
using namespace std;
#include "queue.h"
main() {
Queue b;
cout << "attempting to access an empty queue\n";
cout << b.head() << "\n";
cout << "after accessing the empty queue\n";
}
When it is executed it will break the assertion in head:
attempting to access an empty queue queue.h:15: char Queue::head(): Assertion `! empty()' failed IOT trap/AbortNotice that the output command after the call on head is never executed. Crashing programs might not be desireable under all circumstances so it can be used for testing and later disabled; if the symbol NDEBUG is defined before the assert then no code is produced and no checking done.
One other small point to notice about Queue is that although functions are usually publically available, they don't have to be. Here the succ function, that increments indices modulo the length of the array, is private. It is only used internally and not part of the functional interface of Queue objects.
push to add a character
top to return the character at the top
pop to discard the character at the top
empty to give true if there is nothing on the stack and false otherwise.
There is no primitive string type in C++, instead arrays of char have been used, but now the C++ standard has a library that has a type string in a header file of the same name.
The new standard library class string provides a wide range of special functions for extracting sub-strings and other manipulations. In addition there are operations between strings such as assignment, comparison and concatenation; some of this is shown below:
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1("hello");
string s2, s3;
s2 = "world";
s3 = s1 + " " + s2;
cout << s3 << "\n";
}
using namespace std;
string s1="hello",
In C there are many library functions that treat an array of characters as if it is a string to be manipulated as a whole. C++ is the same, the C function declarations can be got by including cstring.h (or for backwards compatibility with C, just string.h. Although the standard now has the string library class it is still important to understand arrays of characters because they (a) are the underlying form of the string, (b) sometimes provide more flexibility and efficiency, (c) are a fundamental part of the language, and (d) probably most existing programs use them instead of the newer library class string.
Since there is no index checking and arrays of any length
can be passed to functions expecting strings the convention is
to terminate the string with a byte containing zero (the null byte)
written as '\0', as an end marker.
The output operator cout « will take
a null terminated character array as argument, :
char line[100];
line[0] = 'h'; line[1] = 'i'; line[2] = '\0';
cout << line;
will print hi;
line must be null terminated otherwise the undefined
values of elements later in the array will be printed.
The standard library of C contains various functions to manipulate arrays of char as if they are strings. To get the correct declarations for them use the header file: string.h or cstring.
#include <iostream>
#include <string.h>
using namespace std;
int main() {
char s1[6] = "hello";
char s2[7] = " world";
char s3[64], s4[64];
strcpy(s3,s1);
strcat(s3,s2);
cout << s3 << "\n";
cout << "length of: \"" << s3 << "\" is " << strlen(s3) << "\n";
}
Here are some notes on the use of char arrays
and other library routines:
char s1[6] = "hello";
char s1[6] = { 'h','e','l','l','o','\0'};
The value of an array name by itself, eg: line is the address of the first element of the sequence of storage units allocated for the array (see figure 2). Indexing an array name involves following the pointer and using the index value as an offset into the sequence of element storage locations. No storage location is allocated for the pointer itself, the array name is like a constant pointer used where necessary by the compiler. Note that this is completely different from Pascal, Modula-2 or Ada where the use of an array name by itself stands for the whole array.
When an array is given as argument eg:
``strlen(line)'', the value used is the address of the
first of the element sequence. The formal argument in the function acts as
a pointer variable initialised with the address so all references in the
function through the formal argument access the storage of the
elements of the actual argument.
|
char s[] which
means that it can be passed any size array of characters; there must
be no size for a formal parameter.
char s[100];
cin.getline(s,100);
hi there
'\0' in s[8]; it has not stored
the newline character (note that cin » s or
cin.get(s) will only read in "hi" because
the space is treated as a separator).
void lower(char s[]) {
for(int i=0;i<strlen(s);i++)
if( isupper(s[i]) )
s[i] = tolower(s[i]);
}
#include <ctype.h>
#include <iostream>
using namespace std;
int main() {
int nums[6] = { 25, 4, 77, 2, 11, 1 };
bool changed = true;
while( changed ) {
changed = false;
for(int i=0; i<5; i++)
if(nums[i] > nums[i+1]) {
int tmp = nums[i];
nums[i] = nums[i+1];
nums[i+1] = tmp;
changed = true;
}
}
for(int i=0;i<6;i++)
cout << nums[i] << " ";
cout << "\n";
}
77777
the cat is on the mat
the gnu
walter
67
the cat is on the mat
SAM
char s[100];
cin.getline(s,100);
hi there
'\0' in s[8]; it has not stored
the newline character (note that cin » s or
cin.get(s) will only read in "hi" because
the space is treated as a separator).
A pointer is a variable that holds the address of another object, it points to it. Pointers are declared by preceding the identifier with a ``*'',
int *ip, *ptr; Account *ap;declares three variables, ip and ptr that can point to integer variables, and ap that can point to any sort of Account. Such declarations do not allocate storage for the int or the Account, they are just boxes that can hold addresses.
Storage can be allocated dynamically using the operator new, its argument is the type of the object to allocate, it returns a pointer to the object. Storage is allocated from a pool of storage often known as the heap.
ptr = new int;
ap = new Account("Blackbeard", 1000000.0);
First new is used to allocate storage for one
int, and the address is assigned to ptr.
Then the new operator allocates storage for an Account
object and returns a pointer to it (its address), which is assigned to
ap.
If the object created is of a class type then arguments
can be passed to the constructor when new is called.
Apart from the normal things that can be done with variables: assignment, passing as a parameter, or returning as a result, the only pointer specific operation is to go through it to the object (variable or value) pointed to. The operator is ``*'', for example:
|
If the storage pointed to is an object (ie. it is of a class type) then it can also be accessed; the result of de-referencing will be to get to the object, and components of an object are accessed by dot (``.'') selection, so:
Account *ap;
ap = new Account("Blackbeard", 1000000.0);
cout << (*ap).balance();
the parentheses show that (i) the pointer is de-referenced, then
(ii) the balance() field of the object is called.
There is an abbreviation for the combined operation of
de-referencing and dot selection, it is the ``->'',
right arrow operator:
cout << ap->balance();Note that the left argument of
-> must be a pointer
to a class type object and the right argument must be one
of its fields (methods).
The operator new can allocate a sequence of storage elements, not just one. Then the pointer will point to the first element of the sequence. The number of elements required is given after then element type:
char *cp = new char[100];allocates space of 100 chars and initialises cp to point to the first. If this is done then pointer arithmetic can be used to access later elements in the sequence offset from the one pointed at (it is scaled correctly by the size of the elements). So the storage can be accessed through the pointer, in this case ip, by arithmetic followed by indirection. The code in figure 4 adds 2 (scaled by the size of an int) to the address in ip and then goes through the result to access the third integer in the allocated block. This gives the same effect as dynamic arrays (in fact there is almost no difference between arrays and pointers, see sub-section 6.3). This gives a lot of flexibility in storage allocation, it means that storage requirements can be computed dynamically and the correct amount of storage allocated.
The address of any lvalue (ie. variable designating expression)
can be taken with the address operator: ``&'',
which returns a value of type ``pointer to whatever the type of
the variable was''. Variables can be declared has holding
pointers to another type by preceding the name in a
declaration by ``*'', so:
int x,y,*ip1,*ip2; x=3; ip1 = &x;
|
|
0
|
declares x and y to be ints and ip1 and ip2
to be pointers to ints, it assigns 3 to x then assigns the
address of x to ip1.
Note that the type of &x is int *, a pointer
to an int, therefore the assignment is type correct,
however the assignment:
ip1 = x;is a type error: ip1 requires an
int * value and x
is just an int.
As the type of ip1 is the same as that of ip2 it can be freely assigned:
ip2 = ip1;
makes both ip1 and ip2 point to x. The value pointed at
by a pointer can be accessed by ``following'' the pointer using the
indirection operator ``*'', this can only be applied to a pointer
and returns the lvalue pointed at, so:
y = *ip2;
*ip1 = 100;
follows ip2's pointer to the location of x, extracts the value: 3
and assigns it to the int y; then it sets x's value to 100.
This operator must be used with caution as it can produce very tangled storage where plain variables can be accessed via pointers and pointers point to the same object. It can also produce ``dangling pointers'' when a pointer is set to a local variable which is later lost when the function containing it returns.
Arrays and pointers in C and C++ are semantically very similar, the only difference being that arrays have storage allocated at compile time and cannot be made to point anywhere else; this sub-section will explain the similarity. The value of an array name is the address of the first location of the storage allocated to it, which is type equivalent to a pointer. In figure 5
|
*(pi+2) = i;
sets the third element of a to 6 as pi points to the first
element and the expression pi+2 points to the third and *(pi+2)
goes indirect through it. It is equivalent to a[2]=i.
Since arrays are similar to pointers, array
names can be used in pointer expressions and pointers can be indexed:
*(pi+9) = 3;
*(a+9) = 3;
pi[9] = 3;
a[9] = 3;
are legal and equivalent.
The only difference between pointers and arrays is that the array
name cannot be assigned to since it only functions as a constant pointer
to its storage and no location for the pointer exists (unlike real pointers).
So although pi = a is ok, a = pi is impossible.
Since it is possible to index sequences of dynamically allocated
variables using a pointer it is in effect a dynamic array,
see the following exercise.
Write a program to sort some floating point numbers (doubles). The program should read a number from the standard input to determine how many numbers to sort. It should use new to allocate a suitable amount of storage. Then read into the dynamic storage the right number of numbers. Finally it should sort them and output them. In other words it is using a pointer and new to produce a dynamically allocated array.
The implementation of many data abstractions requires sequences of data items to be stored, the commonest, and usually the most appropriate, form of data type to use for this is an array. However there are situations when an array is not the best structure, if insertions or deletions have to take place in the middle of the sequence then there is a serious overhead of moving all later elements up or down. Instead linked lists can be used, where each element of the sequences has a pointer to the next element of the sequence:
Each element is an object containing the value to be stored and a pointer to the next object, for example:
class Cell {
public:
Cell(int v, Cell *n) {val=v; next=n;}
int val;
Cell *next;
};
here the list cell holds an integer and a
pointer field to objects of the same type, Cell *.
The class makes both data fields publically accessible.
There is also a constructor even though the class
is so simple, this is to simplify the setting
of fields when an object is created:
rep = new Cell(1, rep);
which (if rep is of type Cell*)
inserts a new cell with value 1 in front
of whatever rep previously pointed to.
The two diagrams in figure 7 show
the creation and insertion of a new cell into the front
of a list.
A simplified class to manage a linked list might be as follows:
#ifndef _LISTH
#define _LISTH
#include <assert.h>
class List {
private:
class Cell {
public:
Cell(int v, Cell *n) {val=v; next=n;}
int val;
Cell *next;
};
Cell *rep;
public:
List() { rep=0; }
bool empty() {return 0==rep; }
void putfirst(int i) { rep=new Cell(i,rep); }
int first() { assert(! empty()); return rep->val; }
void butfirst() { assert(! empty()); rep = rep->next; }
};
#endif _LISTH
The operations are chosen to be similar to those
on lists in functional languages like Lisp, Miranda or Haskell.
They allow elements to be added at the front, putfirst,
removed from the front butfirst and for the front
value to be accessed, first. In this version
all the functions are defined in the class because they are so
short they are suitable for in-line expansion.
The class can be used very simply:
#include <iostream>
using namespace std;
#include "list.h"
int main() {
List s;
s.putfirst(2); s.putfirst(1); s.putfirst(8); s.putfirst(1);
cout << "front should be 1, it is: " << s.first() << "\n";
s.butfirst();
cout << "butfirst, front should now be 8, it is: "
<< s.first() << "\n";
}
If a remove or delete function is needed
it will have to go in a separately compiled file
because it is to long to be in-line expanded. So
first add the prototype declaration to List:
class List {
public:
List() { rep=0; }
...
void butfirst() { assert(! empty()); rep = rep->next; }
void remove(int i);
private:
and then a separate file list.cc that contains the
full definition of remove:
#include "list.h"
void List::remove(int i) {
Cell *ptr = rep;
if(0==ptr) return;
if(ptr->val==i) {
rep = ptr->next; delete ptr; return;
}
while(ptr->next != 0) {
if(ptr->next->val == i) {
Cell *t = ptr->next;
ptr->next = ptr->next->next;
delete t;
return;
}
ptr = ptr->next;
}
}
this is so complicated because (i) the first case
of an empty list must be checked, (ii) if the element
to be removed is in the first cell then the ``root'' pointer rep
must be altered, (iii) in the main loop it is
necessary to examine one ahead, ptr->next->val,
so the ptr->next pointer
of the current cell can be changed, and (iv) it is necessary
to save a copy of the changed pointer so it can be deleted
and returned to free storage.
Design and implement a class to provide a set of integers. The set should behave a bit like the simple mathematical idea of a set, there is no concept of order and elements belong uniquely to a set, so that if 42 is in a set it is in once no matter how many times it is added. The operations that should be provided on this abstract type are add, member which tests whether a value is in the set, and remove to remove a value.
The implementation can be done however you wish but clearly it is possible to use linked storage (that is why the exercise is suggested here) or an array. Given the existence of the List class it should be possible to use it two or three different ways:
In the first version of the list, in order to examine all the elements stored in it, the list must be destroyed; to access the second element the first must be removed, etc. This can obviously be remedied by adding some new accessors but there is a different form of solution called an iterator. An iterator is an object associated with a container storage object, like a list, that allows each element in the container object to be examined in turn without destroying the container. Iterators are ``throw-away'' objects in that they are created for one inspection of the list and then discarded, the next time the list is examined another will be created. First the new header file list.h:
#ifndef _LISTH
#define _LISTH
#include <assert.h>
class List {
private:
class Cell {
public:
Cell(int v, Cell *n) {val=v; next=n;}
int val;
Cell *next;
};
Cell *rep;
friend class Iterator;
public:
List() { rep=0; }
bool empty() {return 0==rep; }
void putfirst(int i) { rep=new Cell(i,rep); }
int first() { assert(! empty()); return rep->val; }
void butfirst() { assert(! empty()); rep = rep->next; }
};
class Iterator {
private:
List::Cell *ip;
public:
Iterator(List l) { ip=l.rep; }
bool done() { return 0==ip; }
int get() { assert(! done()); return ip->val; }
void step() { assert(! done()); ip = ip->next; }
};
#endif _LISTH
The code to use Iterator is in test-list.cc which builds a list and then uses an iterator i to examine it:
#include <iostream>
using namespace std;
#include "list.h"
int main() {
List s;
s.putfirst(2); s.putfirst(1); s.putfirst(8); s.putfirst(1);
cout << "iterating from front to back, should see 1 8 1 2\n";
for(Iterator i(s); ! i.done() ; i.step() )
cout << i.get() << " ";
cout << "\n";
}
Now re-implement the integer set exercise using the second approach:
take the List type and use it unchanged as the internal representation of the Set, and have very simple methods for Set that manipulate the internal List member.You will need the iterator for lists in order to implement the member function. Also provide a SetIterator implemented using the list Iterator. The test program can be as before but add some code to try out the SetIterator.
The decision to use linked storage or an array to implement some data abstraction depends on how the structure will be accessed and used.
A reference to a value is an alias for it (a hidden pointer).
Preceding a name by & in a declaration makes it
a reference (NB a reference is not a pointer and must not be confused
with the & operator, although behind the scenes references
might be implemented using addresses, at the language level
there is NO connection between pointers and references,
the are separate concepts).
Given:
double d = 3.0;
double &ali = d;
double e = ali;
..
ali = ali + 0.5;
declares ali to be a reference to doubles and initialises
it to refer to d (references can only be set by initialisation).
All subsequent mentions of ali can only access
whatever is referred to, ie. d (so unlike
pointers the reference cannot be manipulated), e is initialised
with d ie. 3.0, ali=ali+0.5 takes the value from d and
assigns d, e is not touched.
Despite the preceding example NEVER use references
in that way, there is no point and it makes programs
unnecessarily hard to understand if there are two ways to
refer to a variable when there is no pressing need. The
following sections show other more sensible ways to use pointers.
When used for the parameters or for the results of functions references are useful2. Given the function:
void swap(int &a, int &b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int x=100, y=200;
swap(x,y);
...
swap really does exchange the values in x and y. This
is because when the function is called and the formal arguments are
initialised they are made aliases for their actual arguments and
subsequent use of a and b in the function actually accesses
x and y. The argument setting during the
call is like: int &a=x which makes a an alias for x.
This provides argument passing just like Fortran, the VAR
parameters of Pascal and Modula-2, or the in out parameters
of Ada.
Argument passing with references can be useful to allow modification of the actual arguments OR it can be used to avoid copying large values. If it is used just to avoid creating and copying value arguments then the reference argument can be made const so it cannot accidentally be changed:
void dontchange(const bigstruct &a)
{
will just pass a reference to the actual argument and won't allow it
to be altered. The use of const for an
argument means
that code in the function cannot change the value of it. It is a way
of adding extra reliability to a program, if the code is not
intended to alter its argument, the compiler can now check
that it won't do it by accident.
Similarly reference results can be useful. With references programs can have lvalue functions, ie. functions used on the left of assignments.
In the List example of the previous section there is an Iterator function called get that returns the int value of the cell currently pointed to by the iterator, this can be changed to have a reference result:
int &get() { assert(! done()); return ip->val; }
when the function is used to retrieve a value (as an rvalue
expression, meaning on the right hand side of assignment,
or in any expression) it works exactly as before:
for(Iterator i(s); ! i.done() ; i.step() )
cout << i.get() << " ";
it just gets a reference back and then uses the value in it.
However since the result of get is an
alias for a storage box (the val field in the list cell)
it can be assigned to:
for(Iterator i(s); ! i.done() ; i.step() )
i.get() = i.get() * 2;
this means every element stored in the list
will be doubled.
Note that in get to return a reference the result type must be &,
but no special operator is used in the return. If it
is used where a value is needed, the value in the storage referred
to is used (an rvalue), but if used in a context where a variable
is altered (an lvalue), the location referred to is used.
No. Although it would now seem that returning references from objects gives, to client code, hidden pointers to the object's inner secret, private parts (?), it is quite safe. Even using an element of the storage is altered, that is all that happens here. The client code cannot manipulate the reference to look at other parts of the data representation: the runtime integrity of objects is preserved, and the source code independence is retained so the implementation of the class can still be changed without affecting client code
The following small programs are possible solutions to the simple exercises suggested earlier. These small programs are not meant to be perfect examples of programming style, they might at best be examples of not totally imperfect C++.
For the first exercise:
// generate and print the Fibonacci
// numbers less than 10000
#include <iostream>
using namespace std;
int main() {
int last, last_but_one, new_last;
last_but_one = 0;
last = 1;
while ( last < 10000 ) {
cout << last << " ";
new_last = last + last_but_one;
last_but_one = last;
last = new_last;
}
cout << "\n";
}
And for the second version:
// generate and print the first 18
// Fibonacci numbers
#include <iostream>
using namespace std;
int main() {
int count=1, last=1, last_but_one=0, new_last;
while ( count <= 18 ) {
cout << last << " ";
new_last = last + last_but_one;
last_but_one = last;
last = new_last;
count = count + 1;
}
cout << "\n";
}
// read and write characters converting all
// upper case to corresponding lower case
#include <iostream>
using namespace std;
int main() {
char c;
while ( cin.get(c) ) {
if ( c >= 'A' && c <= 'Z' )
cout.put('a' + c - 'A');
else
cout.put(c);
}
}
The first exercise:
// a function to return the larger of 2 numbers
// and a simple main program to call it & test it.
#include <iostream>
using namespace std;
extern int larger(int,int);
int main() {
cout << "larger of 67 and 4 =" << larger(67,4) << "\n";
cout << "larger of 4 and 67 =" << larger(4,67) << "\n";
cout << "larger of -8 and -1 =" << larger(-8, -1) << "\n";
cout << "larger of 5 and -10 =" << larger(5,-10) << "\n";
}
int larger(int a, int b) {
if ( a >= b ) return a;
else return b;
}
The second exercise sample answer:
// a function to find the greatest common
// divisor of 2 integers
// and a simple main program to call it & test it.
#include <iostream>
using namespace std;
extern int gcd(int,int);
int main() {
int a,b;
cout << "gcd: input 2 integers, to stop input 0 0\n";
cout << "2 ints: ";
cin >> a >> b;
while( a != 0 || b != 0) {
cout << "gcd of " << a << "," << b << "=" << gcd(a,b) << "\n";
cout << "2 ints: ";
cin >> a >> b;
}
}
int gcd(int x, int y) {
int xx = x, yy = y;
while (xx != yy)
if (xx > yy)
xx = xx - yy;
else
yy = yy - xx;
return xx;
}
The first exercise:
#include <iostream>
using namespace std;
class Accumulator {
private:
double total;
int count;
public:
Accumulator() { total = 0.0; count=0;}
void add_in(double n) {
total = total + n; count++;
}
int number() { return count; }
double getTotal() { return total; }
double mean() { return total/count; }
};
int main() {
Accumulator acc;
double x;
while(cin >> x) acc.add_in(x);
cout << "mean of " << acc.number()
<< " numbers is " << acc.mean()
<< " sum = " << acc.getTotal() << "\n";
}
The second exercise:
#include <iostream>
using namespace std;
class MaxMin {
private:
int lsf, ssf;
int count;
public:
MaxMin() { count=0;}
void accept(int n) {
count++;
if(count==1) lsf = ssf = n;
else {
if(n > lsf) lsf = n;
if(n < ssf) ssf = n;
}
}
int number() { return count; }
int largestSoFar() { return lsf; }
int smallestSoFar() { return ssf; }
};
int main() {
MaxMin mm;
mm.accept(56); mm.accept(3);
mm.accept(4); mm.accept(17);
mm.accept(23); mm.accept(34);
cout << "number of numbers= " << mm.number()
<< " largest= " << mm.largestSoFar()
<< " smallest = " << mm.smallestSoFar() << "\n";
}
The first exercise:
#include <iostream>
using namespace std;
int main() {
char line[132], c;
int i, nxt=0;
do{ cin.get(c);
line[nxt] = c;
nxt++;
} while(c != '\n');
// start at nxt-2 to avoid the newline char
for(i = nxt-2; i >= 0; i--)
cout.put(line[i]);
cout.put('\n');
}
The first exercise, a simple stack class. First the header file stack.h:
static const int MAXSIZE=256;
class Stack {
private:
char mem[MAXSIZE];
int topp;
public:
Stack(void) { topp=-1; }
int empty(void) { return topp<0; }
int full(void) { return topp==MAXSIZE-1; }
char top(void) {
assert(! empty());
return mem[topp];
}
void pop(void) {
assert(! empty());
topp--;
}
void push(char c) {
assert(! full());
topp = topp+1;
mem[topp] = c;
}
};
Now the test program that includes the header file:
#include <iostream>
using namespace std;
#include "stack.h"
int main() {
Stack filo;
char c;
cin.get(c);
while(c != '\n') {
filo.push(c);
cin.get(c);
}
while(! filo.empty()) {
cout.put(filo.top());
filo.pop();
}
cout << "\n";
}
The second exercise, a simple frequency table. First the class definition in header file freqtab.h:
#include <assert.h>
class FreqTab {
private:
int icounts[10];
public:
FreqTab() {
for(int i=0; i<10; i++) icounts[i]=0;
}
void accept(int n) {
assert(0 <= n && n <100);
icounts[n / 10]++;
}
int interval(int i) {
assert(0 <= i && i <10);
return icounts[i];
}
};
Next a small test program to read numbers,
put them in a frequency table and finally retrieve
the frequencies.
#include <iostream>
using namespace std;
#include "freqtab.h"
void printStars(int c) {
for(int i=0; i<c; i++) cout << "*";
}
int main() {
FreqTab ft;
int n;
while(cin >> n)
if(n >= 0 && n < 100) ft.accept(n);
for(int i=0; i<10; i++) {
cout << i << ": ";
printStars(ft.interval(i));
cout << "\n";
}
}
The first exercise, to sort some numbers:
// function to sort an array of integers
// and a test program
#include <iostream>
using namespace std;
extern void sort(int, int[]);
int main() {
int nums[6] = { 25, 4, 77, 2, 11, 1 };
sort(nums, 6);
for(int i=0; i<6; i++)
cout << nums[i] << " ";
cout << "\n";
}
void sort(int n[], int count) {
bool changed = true;
while( changed ) {
changed = false;
for(int i=0; i<count-1; i++)
if(n[i] > n[i+1]) {
int tmp = n[i];
n[i] = n[i+1];
n[i+1] = tmp;
changed = true;
}
}
}
The second exercise to check whether lines of text
are in ascending order:
#include <iostream>
#include <string.h>
using namespace std;
int main() {
char line1[132], line2[132];
bool inorder=true; /* assume in order */
line1[0] = '\0'; /* the empty string is lexically
prior to any others */
while(inorder && cin.getline(line2,132)) {
if(strcmp(line1,line2) > 0)
/* > 0 means 1st arg is lexically later than 2nd */
inorder = false;
strcpy(line1,line2);
}
if(inorder) cout << "In Order\n";
else cout << "Out of Order\n";
}
#include <iostream>
using namespace std;
extern void sort(int, double[]);
int main(void) {
int how_many;
double *nums;
cout << "how many numbers? > "; cin >> how_many;
nums = new double[how_many];
cout << "now input " << how_many << " reals\n";
for(i=0;i<how_many;i++) cin >> nums[i];
sort(how_many,nums);
for(int i=0; i<how_many; i++) cout << nums[i] << " ";
cout << "\n";
}
void sort(int count, double n[]) {
bool changed = true;
while( changed ) {
changed = false;
for(int i=0; i<count-1; i++)
if(n[i] > n[i+1]) {
double tmp = n[i];
n[i] = n[i+1];
n[i+1] = tmp;
changed = true;
}
}
}
This is a version of Set that mimics List but because there is no order there are no putfirst and butfirst methods, instead there is a member function to test if an integer is present in the set. Also the add method must not allow elements to be repeated, so it uses member to avoid adding elements twice. First the header file set.h:
#ifndef _SETH
#define _SETH
#include <assert.h>
class Set {
private:
class Cell {
public:
Cell(int v, Cell *n) {val=v; next=n;}
int val;
Cell *next;
};
Cell *rep;
public:
Set() { rep=0; }
bool empty() {return 0==rep; }
bool member(int e);
void add(int i) {
if( ! member(i) ) rep=new Cell(i,rep);
}
void remove(int e);
};
#endif _SETH
Now the code for the methods not defined in the class definition,
file set.cc:
#include "set.h"
bool Set::member(int e) {
Cell *tmp = rep;
while(tmp != 0) {
if(tmp->val == e) return true;
tmp = tmp->next;
}
return false;
}
void Set::remove(int e) {
Cell *ptr = rep;
if(0==ptr) return;
if(ptr->val==e) {
rep = ptr->next;
delete ptr;
return;
}
while(ptr->next != 0) {
if(ptr->next->val == e) {
Cell *t = ptr->next;
ptr->next = ptr->next->next;
delete t;
return;
}
ptr = ptr->next;
}
}
And lastly a simple program to test some parts of the
set. This program deliberately adds a value three times
and then removes it, if the class correctly implemented
unique membership then the value will be gone.
#include <iostream>
using namespace std;
#include "set.h"
int main() {
Set s;
cout << "adding 2 5 8 1 to the set\n";
s.add(2); s.add(5); s.add(8); s.add(1);
if(s.member(5))
cout << "5 should be in the set, it is\n";
else
cout << "panic...\n";
if(s.member(3))
cout << "panic...\n";
else
cout << "3 should not be in the set, it isn't, good\n";
s.add(1); s.add(1); // 1 add-ed 3 times, but should only be in once
s.remove(1); // remove it,
cout << "added 1 twice more, and now removing it once\n";
if(s.member(1))
cout << "panic, 1 shouldn't be there now\n";
else
cout << "1 should not be in the set, it isn't, good\n";
}
This time the integer set is implemented using an internal list. This is completely transparent to client code. This the header file set.h:
#ifndef _SETH
#define _SETH
#include <assert.h>
#include "list.h"
class Set {
private:
List srep;
public:
Set(): srep() { }
bool empty() {return srep.empty(); }
bool member(int e) { // can't be bothered to put this in set.cc
Iterator i(srep);
while(! i.done() ) {
if(i.get() == e) return true;
i.step();
}
return false;
}
void add(int e) {
if( ! member(e) ) srep.putfirst(e);
}
void remove(int e) { srep.remove(e); }
friend class SetIterator;
};
class SetIterator {
private:
Iterator li;
public:
SetIterator(Set s):li(s.srep) { }
bool done() { return li.done(); }
int &get() { return li.get(); }
void step() { li.step(); }
};
#endif _SETH
In this case there is no code file for the extra
methods, because there are no methods not fully
defined in the class. There should be, member
has a loop and ought to be moved out of the class
but I couldn't be bothered.
So on to the test program, which is exactly as before but has a simple function to test the SetIterator.
#include <iostream>
using namespace std;
#include "set.h"
void printset(Set &s) {
SetIterator si(s);
for( ; ! si.done() ; si.step() ) cout << si.get() << " ";
}
int main() {
Set s;
cout << "adding 2 5 8 1 to the set\n";
s.add(2); s.add(5); s.add(8); s.add(1);
cout << "set contents at start: ";
printset(s); cout << "\n";
if(s.member(5))
// as in the previous test
// ....
cout << "set contents at end: ";
printset(s); cout << "\n";
}
© University of Hertfordshire Higher Education Corporation