The following is the table of contents.
1.Objectives
2.Road Map
3.Why Object Oriented Programming?
1.The crisis in software engineering
2.How does Object Oriented Programming (OOP) help?
3.OOP and Client/Server computing
4.OOP and the Job Market
4.Why Abstract Data Types (ADTs)?
1.Stacks
2.The Stack ADT
3.Formal definition of ADT
5.Why C++?
6.An annotated C++ bibliography
7.Incompatibilities between ANSI C and C++
1.Identifiers
2.Function declarations
3.Global data
4.Structs
8. Types
9.Assignment statements
10.Initializations
11.Miscellaneous
12.How to share header files between C++ and C
13.C++ comments
14.I/O, I/O and Off to Work We Go...
1.Standard places to read and write
2.A first look at output
3.A first look at input
4.Formatted input and output
5.Opening and closing files (streams)
6.I/O condition states
7.More on output
8.More on input
9.seek and tell
15.Pass by reference
16.Default arguments
17.Intro to Classes: Abstract Data Types (ADTs)
1.The Stack ADT
2.Declaring the public interface
3.An array implementation of int_stack
4.The main thing about int_stack
5.Declaring the public interface for a linked list int_stack
6.Rewrite main
7.A linked list implementation of stack
18.Inline functions
19.Overloaded functions
1.'+' is an example of an overloaded function
2.Overloading stack
3.Interactions between default arguments and overloaded functions
20.constructors and destructors
21.new and delete vs. malloc() and free()
1.new vs. malloc()
2.delete vs. free()
3.delete[]
22.Overloading operators
1.Operators which can be overloaded
2.Operators which cannot be overloaded
3.An example - complex numbers
23.Friends
24.Template functions and classes
25.Support of object oriented programming: inheritance and virtual functions
1.The virtual base class vstack
2.A derivation of stacks using arrays
3.A derivation of stacks using linked lists
4.The main program
5.The function that uses vstack
26.public vs. protected vs. private access
27.multiple inheritance
28.Designing with objects
1.Rules of thumb
2.Design steps
3.Discovering classes
29.More on const
1.Unlike C, const really is constant in C++
2.const pointer vs pointer to const vs const pointer to const
3.const reference variables
4.const reference variables and arguments to functions
5.const references to class data
6.returning const
7.Linkage of const variables
30.More on reference
1.References to pointers
2.Functions that return lvalues
31.Copy constructors
32.The base/member initialization list
33.The this pointer
34.More on operator overloading
35.User-defined conversion functions
36.Argument matching
37.Catch, throw, and try
38.Potpourri
1.A word about default arguments
2.Debugging constructors and destructors
3.``is a'' vs. ``has a'' vs. ``kind a''
4.resource acquisition is initialization
5.Allocating
small objects
Objectives
At the end of this course on C++ you will:
Have seen all the features in C++ that are not in ANSI C
Know about the (minor) incompatibilities between C++ and C
Have been introduced to abstract data types (ADTs) and have seen how C++ facilitates their use
Have been introduced to object oriented programming and have seen how C++ supports this paradigm
However, you will NOT:
Be comfortable with C++
Be able to write even 10 line programs without a manual
Be an expert on ADTs (that would take 2-3 academic courses)
Be an expert in object oriented programming (ditto, or lots of experience)
Be an expert in object design (ditto)
As an analogy, you'll have seen a training film on how to ride a bicycle, but won't yet have tried one.
There are only three known ways to get proficient at C++:
Practice
Practice
Practice
Road Map
Why Object Oriented Programming?
The crisis in software engineering
A rule of thumb in software engineering is:
What this means is that it takes 4 programmers to write a program that is twice as large as a program written by one
programmer, in the same amount of time.
The crisis occurs because the size of large programs doubles every 2-5 years.
For example, rumor has it that programs like MS Word and WordPerfect currently require 20-30 people to produce a new
version a year, and that these programs are 250-300,000 lines of code. Assuming new versions will double in size every 4
years, gives the following table:
That is, by the year 2001, either nobody will be able to afford to write the next version, or it will take 4 times as long to
produce the next version with the same staff.
How does Object Oriented Programming (OOP) help?
The above rule assumed each program was one large piece of code. Breaking a program up into separate functions doesn't
help because of interactions between functions. The only types of things that can work is if a program can be constructed
from smaller pieces, each of which is completely independent.
This is exactly what object oriented programming is about. Programs are constructed of objects. Each object contains data
and code and is completely self-contained. There are ways of manipulating these objects so that all the ``real world''
manipulations can be done like:
objects can be used as-is
objects can be used mostly as-is but with some extra code for the problem at hand (inheritance and derived objects)
objects can be selected based on criteria known only at run-time (virtual objects)
objects can be generalized to work on other objects whose type is not known until link-time. Consider sorting an
array. You should be able to write an object that sorts arrays no matter what type of object is in each element.
(templates)
OOP and Client/Server computing
Each object is an independent entity that communicates with other objects by sending messages. (To: Sort-array, Message:
sort this array of integers.) This maps very nicely onto Client/Server computing which has client programs (objects) on various
machines interacting with server programs (objects) on other machines via some kind of protocol (messages).
OOP and the Job Market
Microsoft, Borland, Word Perfect, and many other large programming firms are now insisting on object oriented
programming experience, usually in C++, for all their
programming positions.
Why Abstract Data Types (ADTs)?
Abstract Data Types (ADTs) are a higher level way of looking at programming. Prototypical ADTs include lists, stacks, and
sets.
Stacks
Suppose your program needs to keep track of a stack of things. You have at least two choices for ways of implementing
stacks:
As an array
As a linked list
Each method has it's advantages:
Arrays are very efficient on storage, but are difficult to extend, easy to implement
Linked lists have the overhead of a pointer for each item, are easy to extend, but are a bit harder to implement
Depending on the needs of your program, you will pick one of these methods. Unfortunately, you may not be able to
determine which is the best method to use until the middle or end of a project.
The Stack ADT
Ignoring how you implement lists, what sorts of things do you want to do?
void init(stack S, unsigned int nitems)
void push(stack S, item x)
item pop(stack S)
boolean isempty(stack S)
boolean isfull(stack S)
In the best of all possible worlds, you would like to be able to declare things to be of type STACK and ITEM, then choose
at the last possible moment (perhaps when you are linking your final program together) which implementation to use.
This is precisely what ADTs are about.
Formal definition of ADT
An abstract data type (ADT) is a mathematical model with
a collection of operations defined on it. [Aho, 1983]
Why C++?
C++ is the world's most successful object oriented programming language. In [Stroustrup, 1993] Bjarne Stroustrup, the
original author of C++ says:
``C++ did three things [...]
1. It produced code with run-time and space characteristics that competed head-on with the perceived leader in that field:
C. Anything that matches or beats C must be fast enough. Anything that doesn't can and will - out of need or mere
prejudice - be ignored.
2. It allowed such code to be integrated into conventional systems and produced on traditional systems. A conventional
degree of portability, the ability to coexist with existing code, and the ability to coexist with traditional tools, such as
debuggers and editors, was essential.
3. It allowed a gradual transition to these new programming techniques. It takes time to learn new techniques. Companies
simply cannot afford to have significant numbers of programmers unproductive while they are learning. Nor can they
afford the cost of failed projects caused by programmers poorly trained and inexperienced in the new techniques failing
by overenthusiastically misapplying ideas.''
A quote that I am unable to locate said something similar to ``this system took 45,000 lines of Ada, not counting the
comments. The C++ version was 18,000 lines of fully commented code.''
C++ allows old style programming, object oriented programming, and abstract data types. All of these prudishness fit
together fairly nicely in a production environment.
An annotated C++ bibliography
Entsminger, Gary. The Tao of Objects. Redwood City, CA: M&T Books, 1990.
Just like watching tennis on TV improves your own game, reading this book will get you in the proper mindset for
object oriented programming. Highly, highly recommended.
Eckel, Bruce. Using C++. Berkeley, CA: Osborne/McGraw-Hill, 1989.
I haven't seen this book, but it is Entsminger's favorite. Entsminger says ``A good book to explore and study if you
want to get to the heart of C++.''
Ellis, Margaret, and Stroustrup, Bjarne. The annotated C++ Reference Manual. AT&T, 1990.
This is the base document used by the ANSI C++ committee. When you really want the answer, look here. This is
truly a reference manual.
Stroustrup, Bjarne. The C++ Programming Language, 2nd edition. Don Mills, ON: Addison-Wesley, 1992.
This manual is in the style of Kernighan and Ritchies The C Programming Language. An okay manual by the author of
the language.
Hansen, Tony. The C++ Answer Book. Don Mills, ON: Addison-Wesley, 1990.
This is a companion manual to The C++ Programming Language, containing answers to all the problems posed at the
ends of chapters. An okay source of C++ examples.
Lippman, Stanley. C++ Primer, 2nd edition. AT&T, 1991.
As much as I distrust 600+ page books calling themselves ``primers,'' this does baby-step you through some of the
subtle interactions in C++.
Pohl, Ira. C++ for Pascal Programmers. Don Mills, ON: Benamin/Cummings, 1991.
A good book for Pascal programmers.
Stroustrup, Bjarne. A History of C++. In SIGPLAN Notices,vol 28 no 3, March 1993.
A good description of which features were added to C++, in what order, and why they were added. Reading this
helps make sense of some aspects of C++.
Aho, Alfred, Hopcroft, John, and Ullman, Jeffrey. Data Structures and Algorithms. Don Mills, ON:
Addison-Wesley, 1983.
An excellent book on abstract data types (ADTs). Buy anything from these authors.
Nagler, Eric. Learning C++: A Hands-On Approach. St. Paul, MN: West, 1993.
This book baby-steps you through C++ an example at a time. I have three small complaints: the author doesn't know
about array initialization, nor does he know that `const' has been fixed in C++, and it is based on Borland C++ (with
the occasional reference to AT&T's CFRONT). Otherwise
it is an excellent book that may fit your learning style.
Incompatibilities between ANSI C and C++
Most constructs in ANSI C are legal C++. The exceptions are:
Identifiers
C++ adds the following new keywords:
You can not use these names as identifiers in your program.
C++ identifiers can be any length; C reserves the right to restrict the length
Function declarations
In C++, a function must be declared before it can be called; in C, the function is assumed to return an int
In C++, the function declaration f(); means f takes no arguments; in C, it means f can take any number of arguments.
Use f(...) to get the latter in C++
``old style'' function declarations and calls of undeclared functions are anachronisms and may not be supported by all
implementations
Global data
In C++, a global object must be defined exactly once; in C, it may be declared several times without using extern
In C++, a global const has internal linkage; in C, it has external
Structs
In C++, a class (struct) may not have the same name as a typedef declared to refer to a different type in the
same scope
In C++, a struct is a scope; in C, a struct, an enumeration, or an enumerator declared in a struct is exported to
the scope enclosing the struct
Types
In C++, the type of a character constant is char; in C, it is int
In C++, the type of an enumerator is the type of its enumeration; in C, it is int
Assignment statements
In C++, like pre-ANSI C, pointers must be assigned values of the proper type; in C, any pointer can be assigned a
value of type void*
In C++, assignment to an object of enumeration type with a value that is not of that enumeration is considered an
anachronism; ANSI C recommends a warning
Initializations
In C++, goto's are not allowed to jump over initializations, unless the initialization is in a block and the entire block is
bypassed; in C, you can bypass the initialization
In C++, surplus characters are not allowed in strings used to initialize character arrays; in C, they are
Miscellaneous
C++ requires consistency even across compilation boundaries; some C implementations may consider certain
incompatible declarations legal
C++ allows fewer variations in conforming implementations than C does
How to share header files between C++ and C
Do not declare any classes, overloading, etc
Do not use names as both structure tags and some other type
If function f takes no arguments, declare it f(void)
Global consts must be declared explicitly as either static or extern
Use the predefined C++ preprocessor name __cplusplus (two underscores) to distinguish information for C++ from
that for C
Functions that are to be callable from both languages must be declared to have C linkage:
extern ``C'' double sqrt(double);
or
extern "C" {
double sqrt(double);
double atan2(double, double);
}
C++ comments
In C, there is just one style of comments: text surrounded by /* and */.
In C++ there are two styles of comments: the C style and text between // and the end of a line.
I/O, I/O and Off to Work We Go...
In C++, input and output are first class citizens; in C, they were no better than any other library function. When reading things
in C you usually have to put an & in front of the name to pass the address. However, some things like strings are already
addresses, so you don't put the &. C++ fixes all of this.
Standard places to read and write
In C, you have three files opened for you: stdin, stdout, and stderr.
In C++, you have four streams opened for you: cin, cout, cerr, and clog. (cerr is an unbuffered connection to stderr,
clog is buffered connection.)
1. A first look at output
Let's start with an example:
#include <iostream.h>
main() {
cout << "Hello, world!\n";
}
<< is an operator that evaluates left to right. It takes an ostream on the left and an object on the right, outputs the object then
returns the ostream as its result. All this is necessary
to make the following program work:
#include <iostream.h>
main() {
char c='0';
short h = 1;
int d = 2;
long ld = 3;
float f = 4.0;
double g = 5.0;
char s[] = "six";
cout << c << h << d << ld
<< f << g << s
<< endl;
}
endl (endline) is a predefined ostream operation that both inserts a newline and flushes the buffer. The output is:
012345six
2. A first look at input
>> is an operator that evaluates left to right. It takes an istream on the left and an object on the right, inputs the object then
returns the istream as its result. If the input operation
fails it returns 0. The following program:
#include <iostream.h>
main() {
char c;
short h;
int ;
long ld;
float f;
double g;
char s[100];
cin >> c >> h >> d >> ld
>> f >> g >> s;
cout << c << h << d << ld
<< f << g << s
<< endl;
}
And the input:
01 2 3 4 5six
Produces the same output as the previous program. Notice that you did not have to put &'s in front of c, h, d, ld, f, and g as
you would have in C.
If the input operation fails it returns 0. This allows
the following program to work:
#include <iostream.h>
main() {
int d, sum = 0;
while( cin >> d )
sum += d;
cout << "The sum is " << sum
<< endl;
}
Given the same input as above it produces:
The sum is 15
3. Formatted input and output
To get formatted I/O using iostream.h use the following functions:
#include <iostream.h>
#include <math.h>
main() {
int i = 1024;
cout << "i (decimal) = " << dec <<
i << endl;
cout << "i (hexadecimal) = " << hex <<
i << endl;
cout << "i (octal) = " << oct << i
<< endl;
cout << "You owe me: $"; cout.precision(2);
cout.fill('*'); cout.width(9);
cout.setf(ios::fixed,ios::floatfield);
cout << 123.45 << endl;
cout << "Precision: " << cout.precision()
<< endl
<< "Width: " << cout.width() << endl
<< "Fill: " << cout.fill() << endl;
// Note width gets reset to 0
cout.setf(ios::scientific,ios::floatfield);
cout.precision(10);
cout << "sqrt(2.0) = " << sqrt(2.0) <<
endl;
cout << "i (still octal) = " << oct <<
i << endl;
}
This produces the following output:
i (decimal) = 1024
i (hexadecimal) = 400
i (octal) = 2000
You owe me: $***123.45
Precision: 2
Width: 0
Fill: *
sqrt(2.0) = 1.4142135624e+00
i (still octal) = 2000
Opening and closing files (streams)
opening and closing files is very easy.
#
include <fstream.h>
main() {
char ch;
ifstream inf("data");
ofstream outf;
fstream inoutf("database",ios::in+ios::out);
outf.open("results");
if( inf.good() ) {
while( inf.get(ch) ) {
outf.put(ch);
}
}
inf.close();
outf.close();
inoutf.close();
}
All types of stream constructor take a second parameter called the open_mode.
4. I/O condition states
after opening or after any I/O operation you can test the status of a stream:
More on output
The following functions can be used on ostreams and iostreams:
seek and tell
The following functions can be used on istreams, ostreams and iostreams:
Pass by reference
In C, everything passed to a function was passed by value. That is, a copy of the parameter is made and the copy is passed.
That means that a function like swap has to be called
carefully:
#include <stdio.h>
void swap(int *a, int *b)
{
int t = *a; *a = *b; *b = t;
}
main() {
int i = 5, j = 7;
swap(i, j); /* Nothing happens to i and j */
printf("i = %d, j = %d\n",i,j);
swap(&i, &j); /* This works */
printf("i = %d, j = %d\n",i,j);
}
This produces the output:
i = 5, j = 7
i = 7, j = 5
C++ adds a new way to pass variables: by reference:
#include <iostream.h>
void swap(int &a, int &b)
{
int t = a; a = b; b = t; // looks more normal
}
main() {
int i = 5, j = 7;
swap(i, j); // This works!
cout << "i = " << i << " j = " <<
j << endl;
}
This produces the output:
i = 7, j = 5
There is no inefficiency in C++ doing this. The code generated
for the call and execution of swap is likely to be exactly the
same for the two programs above.
Default arguments
C++ allows you to provide default values for arguments to functions. These must be the righmost arguments. Any or all of
them can be left out.
typedef enum {false, true} boolean;
void draw_axis(double Xlen, double Ylen,
double Xmin = 0.0, double Ymin = 0.0,
double Xscale = 1.0, double Yscale = 1.0,
boolean Xlogarathmic = false,
boolean Ylogarithmic = false,
boolean full_grid = false);
Now,
draw_axis(10.,10.);
Will draw a set of axis. However, if you want to mention the Yscale:
draw_axis(10.,10.,0.0,0.0,1.0,0.5);
Will scale the Y axis by 0.5. To get a full grid:
draw_axis(10.,10., 0.,0., 1.,1., false, false, true);
Don't let the complexity of this example throw you off. Default arguments are very useful. For example fstream.h defines
open for ostream as
void open(const char *name, int mode=ios::out, int prot=0664);
So ostream's open up by default with mode=ios::out and prot set so that you and your group can read and write the file
and others can just read it (the operating system may over-ride this). Since these are reasonable defaults, most of the time you
won't change them nor will you have to specify them.
Beware! There is a subtle interaction between default arguments and overloaded functions that we will talk about when we
know about them both.
Intro to Classes: Abstract Data Types (ADTs)
C++ supports ADTs through information hiding and definitions of public interfaces. To see what this means, let's revisit the list
ADT we first met in chapter 6:
The Stack ADT
Ignoring how you implement lists, what sorts of things do you want to do?
void init(stack S, unsigned int nitems)
void push(stack S, item x)
item pop(stack S)
boolean isempty(stack S)
boolean isfull(stack S)
For now, let's assume all items are integers. (We'll make our ADT more general when we learn about templates.)
Declaring the public interface
File: mydefs1.h
Note:
classes look a lot like structs. In fact, C++ treats structs as a class whose members are by default public
by default, members of classes are private. We could say private after line 3, if we wanted to make this explicit
An array implementation of int_stack
File: stack1.C
Note:
We'll talk about new (line 10) later. It essentially does a malloc(sz*sizeof(int)).
assert( exp ) does nothing if exp is non-zero, else it raises a run-time exception
Note the fully qualified names must be given (int_stack::init et al)
The main thing about int_stack
File: stackmain1.C
Note:
It's short!
I've tested it by pushing 501 things onto s1 and by adding another s2.pop() after line 22
The (correct) output is:
19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
If we forget to call init, we are in big trouble
We haven't provided any way to get rid of a stack
Declaring the public interface for a linked list int_stack
File: mydefs2.h
Note:
The public part of the interface looks the same
Rewrite main
File: stackmain2.C
Note:
There is but a one character change, on line 2!
This has been tested with 21 pops at line 16. It correctly detects the empty stack.
A linked list implementation of stack
File: stack2.C
Note:
line 9 makes sure there was free store available
nitems is completely ignored by init (lines 14-17)
delete t works like free( t )
Some of these functions sure are short!
Inline functions
As we have seen above, lots of useful functions are very short. To incur the 5-20 machine instructions associated with
function call and return seems awfully expensive. (This was why the old FORTRAN technique of writing everything in main
got developed. Ugh.)
To address this, C++ allows you to put explicitly the inline modifier on function calls. For example:
inline int abs( int a )
{
return a < 0 ? -a : a;
}
C++ treats this as a suggestion only. The compiler is free to implement this as a true function. However, most implementations
will expand this function in-line, so no function call and return is done.
Contrast this with the C preprocessor method:
#define ABS(A) ( ((A) < 0) ? -(A) : (A) )
(I believe all those brackets are necessary to avoid unwanted side-effects.)
You can also implicitly inline functions inside classes (these functions are referred to as methods in the C++ literature) by
defining them inside the definition. ie.
File: mydefs1a.h
Note:
This is an extreme case! Everything is done inline. (There is no stack1a.C)
Need to make a modified version of stackmain1.C that includes mydefs1a
Let's take a look at the linked list implementation too:
File: mydefs2a.h
File: stack2a.C
Overloaded functions
When two functions have the same name, but are distinguished by the type of their operands or the type of the value they
return, they are said to be overloaded. This simplifies programming, and is something you are already very familiar with.
'+' is an example of an overloaded function
Take the simple C assignment:
a = b + c;
What does it do? There are several possibilities:
It adds signed integers b and c then stores the result in a signed integer location
it adds doubles b and c then stores the result in a double location
Many other variations to do with adding integers to unsigned integers, or integers to floats or doubles
In spite of the complexity of this description, we would still say + is not that complicated. It takes b and c and ``does some
reasonable form of addition'' on them, then does whatever it takes to store it in location a. The writer of the C compiler has to
worry about all the possibilities; you mostly don't care.
Overloading stack
Suppose we want to have a stack of doubles in our program, as well as a stack of integers.
However, we don't want to use names other than push and pop on each type of stack.
Let's try it:
File: mydefs1b.h
File: stackmain1b.C
Note:
The output of this program is:
19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
t s r q p o n m l k j i h g f e d c b a
push and pop figure out which type of stack they are using
You can write line 14 as push(s1,i) if you find it more natural
Interactions between default arguments and overloaded functions
Consider the following two overloaded functions:
int fa(int i, int j=5);
int fa(int i, char c='e');
What does this mean?
int k = fa(3);
Does it mean k = fa(3,5) or k = fa(3,'e')? C++ compilers are required to flag this as an ambiguous statement and
treat it as an error, but not all compilers do.
constructors and destructors
If your abstract data types are going to be first class citizens, they need to be shown a bit more respect. When you declare an
array or a structure or an integer in a function, C++ allocates the memory for you when the function is called. Similarly, when
the function returns, C++ deallocates the memory. Wouldn't it be nice if you could get access to this?
Note:
We want to be able to say
{
int_stack s1;
// ...
}
// storage for s1 has been deallocated
and not have to do the s1.init();
We don't want to lose the ability to do the functional equivalent of s1.init(500);
which allocates a stack of non-default size.
We would also like to be able to be able to do things like int_stack array_of_stacks[10];
Or even
int_stack *ps;
ps = new int_stack;
// ...
dispose ps;
C++ uses special member functions based on the name of the class. For int_stack the constructor would be called
int_stack::int_stack (compare with int_stack::push etc.); the destructor would be called int_stack::~int_stack.
Let's try it:
File: mydefs1c.h
File: stackmain1c.C
Note:
The (correct) output is:
19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
10 11 12
Using just one constructor as before say, int_stack( int nitems = 100), works for everything except the array
declaration int_stack ... s4[5]. There the default constructor int_stack() gets called 5 times, so we need to declare such
a constructor.
There are three equivalent ways of declaring a stack of non-default size:
int_stack s1(500); // or
int_stack s1 = 500; // or
int_stack s1 = {500};
Here are some other equivalent declarations:
char s[] = "hello"; // or
char s[] = {"hello"}; // or
char *s = "hello"; // or
char *s = {"hello"};
//
int i = 5; // or
int i(5); // ARM says this works, g++ says it doesn't
int i = {5};
Line 18 of main: The selector -> has been extended to select member functions
Line 13 of the header file uses delete[] which deletes arrays.
new and delete vs. malloc() and free()
new vs. malloc()
If T is some type in your program (char, int, double, or some struct or class) and
T *tp, *ta;
then
tp = new T;
is equivalent to the pre-ANSI C style of:
tp = (T *) malloc(sizeof(T));
Note:
The ANSI C style of tp = malloc(sizeof(T));
doesn't work in C++ because void * (the type returned by ANSI malloc) does not coerce to all pointer types. C++
points out that this is a hole in C's otherwise careful type system.
Similarly,ta = new T[n];is akin to the pre-ANSI C style of:
ta = (T *) malloc(n * sizeof(T));
except
the default constructor for T, T::T(), gets called n times if it exists
the size of the resulting array is saved ``somewhere'' for use by delete[]
Note:
ta = new T[0]; still returns a pointer to an object of type T
if no storage is available new returns 0, so a quick and dirty way of checking is:
ta = new T[n];
assert( ta ); // or even assert( ta = new T[n] );
delete vs. free()
Continuing our example from above,delete tp; is equivalent to the ANSI C free(tp);
Note:
delete[] tp; is not defined when tp is allocated without using the array syntax
delete[]
Continuing our example from above,delete[] ta; is equivalent to the ANSI C free(ta);
Note:
delete ta; is not defined when ta is allocated using the array syntax
deleting parts of arrays or other proper subsets of the memory returned by new is not defined
Overloading operators
Most operators can be overloaded. A few cannot.
Operators which can be overloaded
Note:
Both the binary and unary forms of can be overloaded.
( ) is function call. [] is subscripting.
Defining your own operator ::new allows you to take over the management of the free store. Use it for debugging
only. (What if two programmers on a project rewrite this?)
Operators which cannot be overloaded
An example - complex numbers
File: Complex.H from Gnu g++ distribution (modified for display here)
Note:
Most of the operators are defined in a proper, usable form; this is a good template for your own classes
A more careful definition might declare all of the non-member functions as friends
These declarations declare const functions returning doubles:
double real() const;
double imag() const;
operators follow the usual rules of precedence and order of evaluation (left to right or vice versa). There is no way to
modify these rules. (If you want + to evaluate before times you have to do it using parenthesis.)
In case the meaning of this class is not clear, we can now write programs that look like complex numbers were always
part of C++, like this:
#include <iostream.h>
#include <Complex.h>
main() {
Complex a,b,c(5),d(1,2);
a = c * d;
b = a - c;
cout << "b = " b << endl;
}
Friends
A function that is a friend of class X has access to all of X's private parts. (This is also the '90's definition of friend :-).) It is
particularly useful for the I/O operators >> and << which work on istream& and ostream& objects and so cannot be
member functions of your class. It is also useful for efficient access. Let's revisit our linked list implementation of int_stack,
and see how simple it becomes:
File: mydefs2b.h
File: stack2b.C
Template functions and classes
In one of our array implementation of stacks we had two classes: int_stack and char_stack. This seems like a fair bit of
rather error prone work to have to create a new class of stack for every class of object we might want to put on a stack.
Templates were invented for just such situations as these.
File: mydefs1d.h
File: stackmain1d.C
Support of object oriented programming: inheritance and virtual
functions
One of the promises I made to you at the beginning was that you could put off implementation choices to the very end. We're
now going to declare a pure virtual class called vstack, two derivations (arrays and linked lists), a main program that
declares stacks of both types and passes them one at a time to a function that has no idea which type of stack it is getting and
indeed handles an array_stack on the first call and a linked_stack on the second.
The virtual base class vstack
File: vstack.h
A derivation of stacks using arrays
File: arraystack.h
A derivation of stacks using linked lists
File: linkedstack.h
The main program
File: stackmain.C
The function that uses vstack
File: reverse.C
public vs. protected vs. private access
members of a class can be:
private, the name can only be used by member functions and friends of the containing class
protected, the name can be used by the above and by member functions and friends of derived classes
public, the name can be used by any function
Use these to control visibility in the interface.
multiple inheritance
We'll work through an example of a theoretical University payroll system.
Designing with objects
This section has largely been paraphrased from [Enstlinger,90]and [Stroustrup, 1992].
Rules of thumb
1.Don't expect to know everything before you start
2.Get something working as soon as possible
3.Change the design as you go
4.Try to make your mistakes as early as possible
Design steps
1.Find the concepts (classes) and their fundamental relationships
2.Refine the classes by specifying the operations on them
3.Classify these operations. Consider construction, copying, and destruction
4.Consider minimalism, completeness, and convenience
5.Refine the classes by specifying their dependencies on other classes
6.Consider inheritance
7.Use dependencies
8.Specify the interfaces for the classes
9.Separate functions into private, public and protected operations
10.Specify the exact type of the operations on the classes
Discovering classes
1.Look for external factors; interactions between objects (classes) and the world outside them. These help you to find
the objects themselves. Look for boundaries in the real world. These are often reflected in your objects. Look for
initialization and cleanup. These belong in constructors and destructors.
2.Look for things that are duplicated. Are there are two or more of some object? Might you someday want to add
another one of these objects? Common interfaces belong inside an abstract base type.
3.Look for the smallest common denominator in the system. For a text editor, you sometimes want to edit characters,
words, lines, blocks, files. This gives you a whole set of related objects and relationships.
4.Will this class work in other situations? Is it general? Which things stay the same? Which differ?
5.Look at the data. Group data that seems to belong together. Is this an object? Should this data be hidden or at least
not be carelessly modified? Put it inside an object
6.Write a main program using the objects you have so far. Do you have everything you need?
More on const
const is tricky enough that it deserves a topic of its own.
Unlike C, const really is constant in C++
The following declarations work in C++ but not in C:
const int maxn = 1000;
int a[maxn + 1];
const pointer vs pointer to const vs const pointer to const
When pointers and const are involved in the same declaration, what does it mean? Does it mean the pointer doesn't change
(const pointer)? Does it mean the thing the pointer points at doesn't change (pointer to const)? Or does it mean neither the
pointer nor the thing it points at change? Luckily, it is possible to express all three concepts in C++:
int i = 1, j = 3;
int *const p = &i; // const pointer
const int *q = &i; // pointer to const
const int *const r = &i; // const pointer to const
*p = 5; // okay
p = &j; // error
*q = 5; // error
q = &j; // okay
*r = 5; // error
r = &j; // error
const reference variables
You can have a reference to an object, and tell the compiler that you don't want to use this reference to change the variable.
int i;
const int j = 1;
const int &r = i;
const int &s = j;
i = 2; // okay
j = 2; // error
r = 2; // error
s = 2; // error
const reference variables and arguments to functions
Rather than have a large object pushed onto the stack, you can pass a reference to the object. If you want to indicate that the
function won't be making any changes to the object, you can pass it by const reference. This is probably the most common
use of references, since it acts like an ``efficient call by value:''
void print_large_array( const int &ar[], int n )
or
void print_large_array( const int *&ar, int n )
const references to class data
A class acts like a struct that is passed implicitly to all its member functions. You can indicate that a member function does not
modify the data area of its class by using const, giving the same ``efficient call by value'' for the implicit class argument that the
previous section gave to explicit arguments:
class sample {
int n;
public:
void set( int i ) { n = i; } // Allowed to change n
int get() const { return n } // Not allowed to change n
}
returning const
You can indicate that the return value has some degree of const:
const int n = 1;
const v() { return n; }
const int w() { return n; }
const int *x() { return &n; }
int *const y() { return &n; } // error return of non-`const *'
// pointer from `const *'
const int *const z() { return &n; }
int main()
{
int i = 1;
const int *q = &i; // pointer to const
int *s; // pointer
i = v(); // okay
i = w(); // okay
q = x(); // okay
s = x(); // error assignment of non-`const *' pointer from `const *'
q = y(); // okay
s = y(); // okay
q = z(); // okay
s = z(); // error assignment of non-`const *' pointer from `const *'
}
Linkage of const variables
Unlike C, const variables declared at the outermost level have internal linkage by default:
#include <iostream.h>
const int zero = 0; // Internal in C++, External in C
extern const int one = 1; // External in both C++ and C
static const int two = 2; // Internal in both C++ and C
More on reference
References to pointers
References to pointers can be used in place of ``pointers to pointers'':
#include <iostream.h>
void setstr( char *&var, char *str )
{
var = str;
}
int main()
{
char *s;
setstr( s, "hello" );
cout << "s is '" << s << "'" << endl;
}
The output is:
s is 'hello'
Functions that return lvalues
Lvalues are ``things that can occur on the left-hand side of the assignment operator.'' References allow functions to return
lvalues:
int a[10];
int &f( int i )
{
return a[i - 1990];
}
int main()
{
f(1994) = 7; // Sets a[4] = 7
}
This is also useful for overloading operators; operator= for type T would be of type T &.
Copy constructors
C++ will call the copy constructor for class T when:
An instance of class T is created from an existing instance of class T. For example:
A function is called and an argument of class T is passed by value:
An unnamed temporary variable is created to hold the return value of a function returning an instance of class T
Examples:
class T {
char *p;
public:
T() {};
T( char *s ) { p = strdup( s ); }
~T() { delete p; }
};
T f( T c )
{
return c;
}
int main()
{
T *a = new T( "hello" );
T b;
T c = *a; // copy constructor will be called for c
b = f( *a ); // copy constructor will be called twice,
// once to pass a to f, once to set b
delete a; // The string that b and c point to is gone!
}
C++ will supply a copy constructor for each class that recursively copies member data from the source instance to the
destination. However, if any of the class members are pointers, then this will give you two pointers to the same memory
location. The copy constructor should be of type:
T( const T & )
That is, it is a constructor that can read but not modify (that's why the const is there) another instance of T. C++ passes the
instance by reference for efficiency. For our example:
class T {
char *p;
public:
T() {};
T( char *s ) { p = strdup( s ); }
~T() { delete p; }
T( const T& u ) { p = strdup( u.p ); }
};
The base/member initialization list
C++ has extended the syntax of constructor functions. You can initialize any data member by giving its name followed by a
list of one or more values in parenthesis. These go in a comma separated list after the closing parenthesis for the argument list
for the constructor. A colon (`:') separates the initialization list from the parameters. For example:
class student {
int sid;
char *fname;
char *lname;
double gpa;
public:
student(int id,char *f,char *l):sid(id),fname(f),lname(l),gpa(0){}
};
You can use any values you want, even the results of function calls.
Of course, you could have also written this constructor like this:
student(int id,char *f,char *l){sid=id;fname=f;lname=l;gpa=0;}
Sometimes you have to initialize data members this way. For example, we might decide that a students sid, fname, and
lname should never change after initialization, so we declare them to be const. However, now the only way we can initialize
sid, fname, and lname is through an initialization list:
class student {
const int sid;
const char *const fname;
const char *const lname;
double gpa;
public:
student(int id,char *f,char *l):sid(id),fname(f),lname(l){gpa=0;}
};
Of course, gpa can be initialized using either method.
The this pointer
Every class T has an implicit data member called this, that is provided by the compiler as if you had declared:
T *const this;
It contains the address of the class instance. In early versions of the C++ language this was used a lot. In modern C++
there are very few uses. If you find yourself using the this pointer a lot, you probably are doing things the wrong way. The
major use of this is for writing member functions that manipulate pointers, especially those where you want to use pointers to
link several class instances together. The following example is from Stroustrup, pp 147/8:
class dlink {
dlink *pre; // previous
dlink *suc; // next
public:
void append( dlink * );
// ...
};
void dlink::append( dlink * p )
{
p->suc = suc; // That is, p->suc = this->suc
p->pre = this; // Explicit use of ``this''
suc->pre = p; // That is, this->suc->pre = p
suc = p; // that is, this->suc = p
}
dlink *list_head;
void f(dlink *a, dlink *b)
{
list_head->append(a);
list_head->append(b);
}
More on operator overloading
Here are some points to consider when overloading operators:
No default arguments are allowed
You can only use the same form (unary or binary) as already exists in the predefined operators. That is, you can
overload + as either unary or binary, but ~ must be unary and | must be binary.
new and delete can be overloaded as static member functions. The following:
must be overloaded as member (not friend) functions. (You probably should also treat the other assignment operators
like +=, *=, etc. the same, but C++ won't insist on it.) All the others operators can be defined as either a nonstatic
member or a global friend.
At least one of the arguments must be an instance of the class to which the operator belongs. Note: For member
functions the first argument is implicitly an instance of the class.
Here's how the compiler translates your code from infix into functional notation. @ is the operator being overloaded, X
and Y are class instances:
User-defined conversion functions
Conversion functions convert class objects to other class objects or to one of the predefined types. Here are the rules:
It must be a member (not friend) of the class from which you want to convert
The conversion to type foo is declared as operator foo()
There are no arguments to the function
There is no explicit return type; the return type is implicit in the function name
The function must return a value of the same type as the name of the function
The function very likely will not modify any class members, so you can put const after the (empty) argument list. i.e..
operator foo() const.
For example,
class integer {
int number;
public:
integer( int n = 0 ) : number( n ) {} // constructor
operator int() const { return number; } // convert to int
operator double() const { return (double)number; } // to double
};
int main()
{
integer in;
int j = in; // implicit cast
int k = (int)in; // explicit C-style cast
int l = int(in); // explicit C++-style cast
}
If you wanted to add a conversion to class X you could add the member function:
operator X() const; // convert to X
Argument matching
The following is a simplified version of the argument matching rules for deciding which function to call. For a more complete
set of rules consult either Stroustrup or the ARM.
The function name must be within scope
A set of conversions must exist so that the function could conceivably be called
The best-matching function is the intersection of sets of functions that best match on each argument. Unless this list has
exactly one member, the call is illegal.
The function so selected must be a strictly better match for at least one argument than every other possible function,
otherwise the call is illegal
A function with n default arguments is treated like n+1 functions with different numbers of arguments
Non-static member functions are considered to have an extra argument specifying the object for which it is called
An ellipsis (...) is a match for an actual argument of any type
If there are two sequences of conversions of different length that lead to the correct type (for example,
int->float->double and int->double) then only the shorter sequence will be considered
The following trivial conversions are considered ``best'':
Next best are the integral (char, short int, enum, bit field to int) promotions and conversions from
float to double
Next are the conversions from unsigned to signed, floating to integral, and all the pointer and reference conversions
Next are matches with user-defined conversions
Last are matches with ellipsis
Catch, throw, and try
catch, throw, and try are an elegant exception handling message. Since I do not have access to a compiler that supports
them, the following code should be taken as an idea of how they work, and not as a working program:
#include <iostream.h>
class divide_by_zero;
double divide( double x, double y) throw( divide_by_zero )
{
if( y == 0.0 ) {
throw divide_by_zero;
}
return x / y;
}
int main()
{
double a, b;
cout << "Enter dividend and divisor: ";
while( cin >> a >> b ) {
try {
cout << a << " / " << b " = "
<< divide(a,b) << endl;
}
catch( divide_by_zero ) {
cerr << "Even C++ can't divide by zero!" <<
endl;
}
}
}
Note:
You thow objects and catch types
To catch all possible exceptions use:
catch()
Inheritance can be used to group exceptions
You can declare the list of exceptions a function can throw, although the code works without it. That is, you can
declare divide as:
double divide( double x, double y)
If an exception is not caught, then the function terminates (calling all appropriate destructors) and returns. This process
continues recursively until main is reached. If main doesn't catch the error then the function unexpected() is called,
which has a default meaning of terminate() which calls abort().
Potpourri
A word about default arguments
Only specify default arguments (int foo( n = 0) ) in the declaration (the part that is the candidate for the ``.h'' file); do not
put them in the definition. They are part of the public interface. You do not want to have them declared in two places,
because that is a code maintenance problem.
Debugging constructors and destructors
The following code is extremely useful when trying to figure out which destructor is having problems:
#include <iostream.h>
class Tracer {
char *p;
public:
Tracer( char *s )
{ p = s; cerr << s << " entered" << endl; }
~Tracer()
{ cerr << "exit from " << p << endl; }
};
Here's an example of it's use. (Normally you'd interleave other class declarations):
int main()
{
Tracer a("one");
Tracer b("two");
}
The output is:
one entered
two entered
exit from two
exit from one
``is a'' vs. ``has a'' vs. ``kind a''
There are three kinds of hierarchical relationships in C++: inheritance (deriving one class from another), inclusion (including
one object as a data member of another), and template (generalizing a specific class given a description of a more general
class). The following chart may help:
resource acquisition is initialization
(This is from Stroustrup, section 9.4).
Whenever a function needs to acquire some resource - memory, a file pointer, or whatever - it needs to handle it responsibly.
For example:
void use_file(const char *fn)
{
FILE *f = fopen(fn, ``w'');
// use f
fclose(f);
}
What if something goes wrong while using `f'? Will `f' get closed? How can we guarantee that the call to fclose(f) will
happen? Consider the following code:
#include <stdio.h>
class FilePtr {
FILE *p;
public:
FilePtr( const char *n, const char *a )
{ p = fopen(n,a); }
FilePtr( FILE *pp) { p = pp; }
~FilePtr() { fclose(p); }
operator FILE*() { return p; }
};
void use_file(const char *fn)
{
FilePtr f(fn, "w");
// use f
}
Note that no matter what happens, the destructor ~FilePtr() gets called for f.
Allocating small objects
(This is from Stroustrup, section 5.5.6). Say you want to have lots of small objects around, and you don't want to have the
considerable overhead of allocating and freeing them. You can switch to a type-specific allocator as in the following example.
Note:In the code below:
The builtin new operator must store the size of the object somewhere; it likely allocates 12 bytes per node because of
this, our allocator likely only allocates 8 bytes
You should be able to declare ``static node *nfree'' inside of class node; my compiler wouldn't let me.
Saying ``node *q = new node[NALL]'' would cause infinite recursion
I could not make this work for templates
The code for main is the same as stackmain2a.C (except we #include mydefs2c.h)
File: mydefs2c.h
File: stack2c.C