| ★ wanayoo — archive 1999 http://wilma.cs.brown.edu/courses/cs032/resources/C++tutorial.html | Nouvelle recherche | Portail wanayoo |
Unlike Java, C++ is a fast, powerful, and flexible programming language. It was originally developed by Bjarne Stroustrup at what was then AT&T Bell Labs in the early to mid 1980s. The C++ programming language is derived from the C programming language. It attempts to retain as much of C's syntax as possible while adding most of the OOP features that you know and (have been brainwashed to) love. In that vein, C++ is a very large and complex programming language, designed to support many different programming paradigms. As such, C++ contains tons of features that you probably should never use, and it has many pot-holes that you must learn to avoid.
Java was built as a simple-to-learn subset of C++ for set-top boxes and drooling AOL users. Now, it's time to play in the big leagues.
This Java to C++ transition tutorial gives a overview of the C++ programming language, focusing on the most commonly used features of the language. No guide of this length could begin to discuss the intricacies of this robust language, and this guide does not purport to do so. Instead, it gives students with a background in Java and object-oriented principles a brief yet somewhat thorough introduction to the language. Code examples are used in abundance in order to increase exposure to C++'s syntax and style. Aside from covering the basics of C++, related topics such as debugging tips and makefiles are discussed in brief. This is a short tutorial, not a reference; you will most likely need to gain access to one of the recommended books if you intend to program in C++ for any substantial period of time.
This tutorial was created in the fall of 1997 for use in CS123. It has been slightly modified for CS032. Modifications mostly involve changing from C conventions to STL conventions. This includes using cout instead of printf and using vectors instead of straight arrays. Any comments should be directed to the CS032 TA staff. The CS123 version of the tutorial is here. This tutorial can be used elsewhere as long as the CS123 staff of Brown University is credited for its creation.
Below is a list of several good books that you should either read or refer to if you have any questions.
Bjarne Stroustrup is the creator of the C++ programming language, and this book is the reference that he has written. It is a good reference book, but not one you would sit down and read to learn the language. (There is a copy of this book in the Sunlab.)
Recommended by the CS15 TA staff as an excellent transition.
This book is simpler to read than the Stroustrup book, but is not as good a reference manual.
A book that is not geared for beginners but that is highly recommended once you have a grounding in the language. It covers some of the fine points of good C++ coding and design and avoiding pitfalls particular to the language.
Highly recommended. This book is very practical and contains many examples and exercises. It is also compatible with a variety of platforms. Don't be intimidated by its large size!
For an introduction, let's take a quick look at the canonical first program, "Hello World!", in both Java and C++. If we examine a Java application instead of an applet, the two programs are very similar.
[Hello.java]
package hello; // says that we are part of a package named hello
public class Hello // declare a class called Hello
{
public static void main(String args[]) // declare the function main
// that takes an array of Strings
{
System.out.println("Hello world!"); // call the static method
// println on the class System.out
// with the parameter "Hello world!"
}
}
[Hello.C]
#include <iostream> // include declarations for the "cout" output stream
using namespace std; // the cout stream is in the std namespace
// this tells the compiler to look in the std
// namespace, you can also write std::cout
int main(int argc, char *argv[]) // declare the function main that
// takes an int and an array of strings
// and returns an int as the exit code
{
cout << "Hello world!" << endl; // this inserts "Hello world!"
// and an newline character into the cout
// output stream
}
Pretty similar, eh?
You will already notice a few key changes. The first is that there can be
global functions, functions which are not methods of a class, such as
main. The next thing you may see is
that we have a #include statement. This tells the compiler to
read in a file that usually contains class or function declarations. Third,
notice that the Java program includes a package declaration, whereas C++
has no analagous concept of packages.
Finally, in Java the main method does not return a value, whereas in C++ it returns an integer. In C++, the integer returned is known as the exit code, which signifies whether or not the program terminated successfully. A value of 0 indicates success, and any other value means the program failed. If no value is explicitly returned, it will automatically return a value indicating success.
There are quite a few differences in syntax between how classes and functions are declared in Java and C++. The biggest difference you will notice is that while all function definitions are included in the class declaration in Java, they are usually put in separate files in C++.
First, in Java:
[Foo.java]
public class Foo // declare a class Foo
{
protected int m_num; // declare an instance variable of type int
public Foo() // declare and define a constructor for Foo
{
m_num = 5; // the constructor initializes the m_num
// instance variable
}
}
Then, in C++:
[Foo.H]
class Foo // declare a class Foo
{
public: // begin the public section
Foo(); // declare a constructor for Foo
protected: // begin the protected section
int m_num; // declare an instance variable of type int
};
[Foo.C]
#include "Foo.H"
Foo::Foo() // definition for Foo's constructor
{
m_num = 5; // the constructor initializes the m_num
// instance variable
}
We split the program into two files, a header file (which we gave
the extension .H) and a program file (which we gave the
extension .C). The header file contains the class declarations
for one or more classes, and the program file contains method definitions.
The program file includes the header so that it knows about the
declarations.
Separating the program declaration and definition into two files has several distinct advantages. First, you can easily look at a header file and see the interface for a particular class, without being having to see its implementation. Second, separating the header and program files can speed program compilation when the implementation of a class changes.
The scope operator :: is used when declaring methods.
If I have a class called Foo and it has a method called
myMethod, when defining the function in the .C file, I would call it
Foo::myMethod. The scope operator is needed because a .C file could
contain method definitions for multiple classes, so we need to know for which
class each method is being defined. In the example class below, we can see the
scope operator in use:
[Foo.H]
class Foo {
public:
Foo();
~Foo();
int myMethod(int a, int b);
}; // note the semicolon after the class declaration!
[Foo.C]
#include "Foo.H"
#include <iostream>
Foo::Foo() // scope operator :: helps define constructor for class Foo
{
cout << "I am a happy constructor that calls myMethod" << endl;
int a = myMethod(5,2);
cout << "a = " << a << endl;
}
Foo::~Foo()
{
cout << "I am a happy destructor that would do cleanup here." << endl;
}
int Foo::myMethod(int a, int b)
{
return a+b;
}
It's crucial that you remember the semicolon at the end of a C++ class declaration. Failure to include the semicolon will cause a compile-time error, but not at the end of the class declaration. Often the error will be reported in a perfectly viable file, such as in a header file that you included.
When an instance of a class is created, you frequently want to initialize various instance variables, some of which are objects. In Java this is easy: you can initialize those variables and perform other startup tasks in the constructor. In C++ you can do the same. C++ constructors can take a variety of parameters as in Java, plus there are some special constructors that we will discuss later. In addition, you can initialize instance variables in an initializer list before the rest of the constructor is called. Whether you use initializer lists for this purpose is partially a matter of personal preference. However, you will need to know its syntax: it's needed sometimes, such as when calling superclass constructors.
For the header file Foo.H:
[Foo.H]
class Foo
{
public:
Foo();
protected:
int m_a, m_b;
private:
double m_x, m_y;
};
The following two definitions for Foo's constructor are
functionally equivalent:
[Foo.C] // with initializer list
#include "Foo.H"
#include <iostream>
using namespace std;
Foo::Foo() : m_a(1), m_b(4), m_x(3.14), m_y(2.718)
{
cout << "The value of a is: " << m_a << endl;
}
OR
[Foo.C] // without initializer list
#include "Foo.H"
#include <iostream>
using namespace std;
Foo::Foo()
{
m_a = 1; m_b = 4; m_x = 3.14; m_y = 2.718;
std::cout << "The value of a is: " << m_a << endl;
}
Useless trivia: The order in which the instance variables are initialized is not the order in which they appear in the initializer list, but instead the order in which they are listed in the class declaration.
Useful trivia: Don't use this inside an initializer list. It
doesn't point to this.
In case you're wondering: Learning how to initialize objects requires some concepts and syntax you haven't learned yet. See the variables and memory management sections for more information.
If you were paying attention to the first example in this section, you may
have noticed that we declared a method Foo::~Foo in addition to
Foo::myMethod and the constructor Foo::Foo. The
special method is called a destructor and is executed when an instance
of the class is destroyed. We will discuss it in more detail when we reach the
memory management section.
Both constructors and destructors do not return anything. In addition, destructors take no parameters.
Like in Java, there are 3 levels of protection for class members in C++: public, private, and protected. They act pretty much the same way as they do in Java. Unlike Java, C++ has no notion of package friendliness, as there are no packages. Because of this, protected members are only accessible to subclasses, while in Java the whole package can use protected members. As you've probably noticed, you put members in sections by their protection level. You can have as many sections of each protection level in a class declarations as you would like. If no modifier is specified, the protection level defaults to private.
C++ also has an additional form of control over protection levels called friendship that allows for a finer grain of protection. You will probably not need to use this in the majority of your coding career. To find out more, consult one of the recommended books.
Inlining is a way to make make your program faster. We will not be covering this in detail. If you are interested please look in one of the recommended books or consult a TA.
In Java and C++ you can have more than one function with the same name. C++ uses the types of the parameters to determine which version of the function to call. There are all kinds of rules about when C++ will do implicit casts and other fancy things for you, but if you don't feel like spending a few weeks with Stroustrup learning about them right now, simply avoid ambiguity when you do overloading. How do you do that? If possible only overload on the number of parameters as opposed to the types of the parameters until you have learned all the rules. Or, if possible, call the functions by different names to avoid overloading entirely (OpenGL uses this method).
#include <iostream>
using namespace std;
void Foo::print(int a)
{
cout << "int a = " << a << endl;
}
void Foo::print(double a)
{
cout << "double a = " << a << endl;
}
On an instance "foo" of type "Foo", calling
foo.print(5);int a = 5foo.print(5.5)double a = 5.5Hint for later: When you learn about pointers and start
overloading things so they take either a pointer type or an int, the
symbol NULL is actually an int! This has brought down many a great
C++ programmer. The workaround is to explicitly cast NULL to the pointer type you
want.
You can give default values for parameters of functions in the
.H file. If fewer parameters are passed than the function takes,
it will use the default values. Using default values can sometimes help you
avoid overloading functions or constructors. Note that parameters without
default values must precede all the parameters with defaults; you can't skip
arbitrary parameters in the middle of a function call. For example:
class Foo
{
public:
Foo();
void setValues(int a, int b=5)
protected:
int m_a, m_b;
};
void Foo::setValues(int a, int b)
{
m_a=a;
m_b=b;
}
If we have an instance "foo" of class
"Foo" and we did the following:
foo.setValues(4);
it would be the same as if we had coded:
foo.setValues(4,5);
Inheritance in C++ and Java is pretty similar. Suppose we have a class
B that inherits from a class A:
class A
{
public:
A();
};
class B : public A
{
public:
B();
};
This says that B has a public superclass A;
there are types of inheritance other than public, but they are never used
in real programs.
If you want to pass a parameter to the superclass constructor, you can do it in the initializer list:
[Foo.H]
class A
{
public:
A(int something);
};
class B : public A
{
public:
B(int something);
};
[Foo.C]
#include "Foo.H"
#include <iostream>
using namespace std;
A::A(int something)
{
cout << "Something = " << something << endl;
}
B::B(int something) : A(something)
{
}
Not bad at all, eh? Umm, that is, as long as you don't use multiple inheritance. Multiple inheritance can be a big can of worms so if you think you need to use it or want to learn where to use it please see a TA first.
To better explain virtual functions, examine the following example in Java:
public void someMethod() {
Object obj = new String("Hello");
String output = obj.toString(); // calls String.toString(),
// not Object.toString()
}
The method toString() is defined in class Object
and overridden in class String. In the above example, Java knows
that obj is really of type String, so at it calls the
String.toString() method. (This is polymorphism at
work.) It can resolve which method to call at run-time since in Java, all
methods are virtual. In a virtual method, the compiler and loader
(or VM) make sure that the correct version of the method is called for each
particular object.
As you can imagine, making everything virtual by default adds a little overhead to your program, which is against C++'s philosophy. Therefore, in C++ functions are not virtual by default. If you don't declare a function virtual and override it in a subclass, it will still compile even though the "correct" version of the method may not get called! The compiler may give you a warning, but you should simply remember to do this for any function that you may override later.
The virtual keyword, the opposite of the keyword
final, allows you to say that a function is virtual:
class A
{
public:
A();
virtual ~A();
virtual void foo();
};
class B : public A
{
public:
B();
virtual ~B();
virtual void foo();
};
We advise making almost all methods virtual when writing your code, since making functions virtual usually adds a very small overhead to your program.
Also, you should always make your destructor virtual. If you do not do this then when you call delete the wrong destructor might get called.
Java provides the keyword abstract to declare that a method
is abstract or pure virtual. C++ also provides for making methods pure
virtual. To do this, add the code = 0 after the parameter list
in the function declaration.
For example, here are the Java and C++ equivalents of making a method pure virtual. First, in Java:
public class Foo
{
public abstract int abstractMethod();
}
And then in C++:
class Foo
{
public:
virtual int abstractMethod() = 0; // The "virtual" and "= 0" are the
// key parts here.
}
Just like in Java, a class derived from Foo cannot be instantiated unless all pure virtual functions have been defined. Also like Java, intermediate abstract subclasses that don't define their parent's pure virtual methods need not list them in their header file.
Say we have a class A and its subclass B. Say
that they both have a virtual function foo and B
wants to call A's foo. In Java, you would use the
super command to use A's foo from
B's. However, C++ has multiple inheritance, so we need another
way to specify which foo to call. The scope operator
:: allows us to do this:
[Foo.H]
class A
{
public:
A();
virtual void foo();
};
class B : public A
{
public:
B();
virtual void foo();
};
[Foo.C]
#include "Foo.H"
#include <iostream>
using namespace std;
A::foo()
{
cout << "A::foo()" << endl;
}
B::foo()
{
cout << "B::foo() called" << endl;
A::foo();
}
So, if we have an instance "b" of class
"B", calling
b.foo();
will output
B::foo() called
A::foo() called
In C++, variables are declared in exactly the same way as in Java. Declaration of an integer variable would look like this under both languages:
int myNumber;
You can also assign values to local variables at the time of declaration, just as in Java:
int myNumber = 0;
(Instance variables can not be assigned a value when declared in the header file; they are initialized in the constructor instead.)
As you see above, C++ and Java declare base type variables in basically the same way. When it comes to declaring variables that can hold a class, however, things get a little more interesting.
Before we go on, we must talk some about memory.
In the introductory courses, Java has shielded you from dealing with computer memory directly, and your TAs did not go too much into it.
A computer is made up of many distinct parts. Among these, the most important ones are the CPU (central processing unit) and memory. If you have a CPU and memory, and throw in some sort of I/O (input/output) device, you have a simple, yet functional computer.
As you might guess, the memory device allows a computer to "remember" things, such as programs and data. The computer remembers everything as numbers in binary form, ones and zeros, on and off switches. A single binary digit is called one bit of information. Computers store everything as bits, including larger data such as strings and classes. Such data types are represented as binary numbers (groups of bits) and stored that way.
You may wonder how a program you write can be compiled into a meaningful series of ones and zeros that your computer understands. Well, it's the job of the compiler to take your program, parse it, and reduce it to special binary numbers called machine language instructions that the CPU on your computer understands. When you run your program, the computer loads these instructions into memory and executes them.
How does memory work?Memory can be thought of as a very large number of "slots." Each slot holds 8 bits, or one byte. The computers you will be working on have 256 megabytes of memory, meaning they have about as many memory "slots." To organize all these slots, you can think of the computer as arranging them in a list. Slot 0 is at the beginning, slot 1 follows it, and so on, until there are no more slots.
---------------------
| Slot 0 |
---------------------
| Slot 1 |
---------------------
| Slot 2 |
---------------------
.
.
.
---------------------
| Slot n-2 |
---------------------
| Slot n-1 |
---------------------
A key thing to realize is that all slots have a unique number associated with them. Referring to "slot 5" is always talking about the same slot.
As mentioned before, each of these slots can hold a single byte. So if we stick a byte into each of those slots, we can say things like "I want to add the byte in slot 7 to the byte in slot 20," or "I want to copy the byte in slot 100 to slot 200." (Such commands are represented by one or more machine language instructions.)
Now that you know how bytes are stored in memory, the next question is how bigger things are stored. Integers, for example, take 32 bits to store. Well, they are just stored as 4 consecutive bytes. Larger types, such as classes, are similarly stored in consecutive memory slots. The computer stores a class in memory by turning it into several numbers. These numbers contain the values of the instance variables in your class and other such information. This class is then stored in memory in a series of consecutive slots.
We can do things with classes that we did with the numbers above. Just as we could say "add the number in slot 7 to the number in slot 20," we can say about classes, "take the class starting at slot 5 and do something to it." Since a class takes several slots, we deal with them in terms of the first slot they occupy. The compiler keeps track of how large each class is so that it knows how many slots after the initial one are used.
What is a memory address?
A memory address is the number of one of the slots mentioned
above.
What is a pointer?
A pointer is a memory address.
So, a pointer to an integer myInt is "the number of
the slot that stores myInt," or more commonly, "the memory address of
myInt."
To declare a pointer to an integer, we place the star operator
* between the data type and the variable name. For example:
int* myIntegerPointer;
One of the uses of the * is to tell the compiler that we want something to
be a pointer when we are declaring it. So the line above means "I want a
pointer to an integer" and not just an integer.
So, now we have a pointer to an integer. However, we didn't assign it a value, so right now it points nowhere. When you declare a pointer, it is pointing to nothing, or worse yet, it often points to a random slot. Therefore, you can not use a pointer without first giving it somewhere to point. Well, you can try using it, but your program will crash with a segmentation fault or a bus error.
How do you make a pointer point somewhere?As we saw earlier, pointers point to data stored in memory. We need to get the
memory address of some data in order to be able to make the pointer point to
it. To get the memory address of something, we use the & symbol.
One of its meanings is "address of."
Now, let's make an integer and have our pointer point to it.
int* myIntegerPointer;
int myInteger = 1000;
myIntegerPointer = &myInteger;
Let's do this in a little program and see what happens.
[main.C]
#include <iostream>
using namespace std;
int main(int argc, char **argv) {
int myInteger = 1000; // declare an integer with value 1000
int * myIntegerPointer = &myInteger; // declare a pointer to an integer
// and make it point to myInteger
cout << myInteger << endl; // print the value of the integer
cout << myIntegerPointer << endl; // print the value of the pointer
}
This program gives the following output:
1000
ffbef4d8
1000 is the value of the integer. ffbef4d
is the value of the pointer in hexadecimal (4290704600),
that is, the memory address of the integer.
Now that we have a pointer to an integer, how can we put it to use? Well, suppose all we had was a pointer to the integer, and we wanted to change the value of the integer to which it points. Before you can actually say something like "set the value of the integer at memory address x to 50," you need to tell the compiler you are talking about the integer at address x, not the address x itself.
For instance, the code myIntegerPointer = 50 does not
mean "set the number that myIntegerPointer points to to 50," but rather "set
the value of myIntegerPointer to 50." This will change the memory address that
myIntegerPointer actually points to; myIntegerPointer will now improperly point
to "slot 50."
In order to modify the integer, we need to dereference the pointer
before we use it. This is where the second use of the "*" comes
in.
myIntegerPointer means "the memory address of
<myInteger>."*myIntegerPointer means "the integer at memory address
<myIntegerPointer>."
Let's modify the example program to show this:
[main.C]
#include <iostream>
using namespace std;
int main(int argc, char **argv) {
int myInteger = 1000;
int *myIntegerPointer = &myInteger;
// print the value of the integer before changing it
cout << myInteger << endl;
// dereference the pointer and add 5 to the integer it points to
*myIntegerPointer += 5;
// print the value of the integer after changing it through the pointer
cout << myInteger << endl;
}
The output is:
1000
1005
This is the expected output. Initially, the number myInteger
has a value of 1000. We then say *myIntegerPointer += 5, which
means "add 5 to the number at memory address
<myIntegerPointer>."
Examine this code:
int myInteger = 1000; // set up an integer with value 1000
int* myIntegerPointer = &myInteger; // get a pointer to it
int mySecondInteger = *myIntegerPointer; // now, create a second integer
// whose value is that of the integer
// pointed to by the above pointer
What will happen if we change the value of myInteger? Will the value of
mySecondInteger change too? Let's see:
[main.C]
#include <iostream>
using namespace std;
int main(int argc, char **argv) {
int myInteger = 1000;
int *myIntegerPointer = &myInteger;
// declare another integer whose value is the same as the integer
// at memory address <myIntegerPointer>
int mySecondInteger = *myIntegerPointer;
// print the value of the first integer before changing it
cout << myInteger << endl;
// dereference the pointer and add 5 to the integer it points to
*myIntegerPointer += 5;
// print the value of the integer after changing it through the pointer
cout << myInteger << endl;
// print the value of the second integer
cout << mySecondInteger << endl;
}
The output is:
1000
1005
1000
So, the answer is no: mySecondInteger is a wholly new integer
at a different memory address. Changing the myInteger
variable has no effect on the mySecondIntegerVariable. By
assigning the value that the pointer points to to another variable, we have
created a copy of that variable's value. Such a result is rarely intended, and
we'll see where this can run you into trouble when we examine pointers to
objects.
Let's print out the addresses of the two integers to be sure that we have a copy. To do this, we need to add the following two lines to the above program:
cout << & myInteger << endl;
cout << & mySecondInteger << endl;
The output is:
1000
1005
1000
ffbef4d8
ffbef4d0
As you can see, the addresses of the two numbers do actually differ.
Can more than one pointer point to the same address?It is possible to have multiple pointers point to the same address. When this happens, changing the value of the number at that address changes the values the other pointers are pointing to, since it is the same address. Let's see an example:
[main.C]
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
int myInteger = 1000;
int *myIntegerPointer1 = &myInteger;
// declare another pointer to the integer above
int *myIntegerPointer2 = &myInteger;
// declare a 3rd pointer. This time, however, make it equal to one of
// the above pointers instead of getting the address again.
int *myIntegerPointer3 = myIntegerPointer2;
// print the values (addresses pointed to) of the pointers
cout << myIntegerPointer1 << endl;
cout << myIntegerPointer2 << endl;
cout << myIntegerPointer3 << endl;
// print the value of the number the pointers point to
cout << *myIntegerPointer1 << endl;
cout << *myIntegerPointer2 << endl;
cout << *myIntegerPointer3 << endl;
// let's change the number...
myInteger = 5000;
// ...and print the values of the pointers again
cout << *myIntegerPointer1 << endl;
cout << *myIntegerPointer2 << endl;
cout << *myIntegerPointer3 << endl;
}
The output:
ffbef4d8
ffbef4d8
ffbef4d8
1000
1000
1000
5000
5000
5000
This shows that all the pointers do indeed point to the same address, and changing the number at that address affects the number all the other pointers point to as well.
Since pointers are just numbers in memory - on our Sparcs, they're 32-bit integers - it's possible to have pointers to these pointers. To see this, let's first declare an integer and a pointer to it as we've done before:
int myInteger = 1000;
int* myIntegerPointer = &myInteger;
Now, let's declare a pointer to the above pointer myIntegerPointer.
This will be a pointer to a pointer to an integer. A pointer to an integer is of
type int *, so a pointer to that will be of type
int **. Making it point to the pointer is a matter of assigning
the pointer's address to our double-pointer:
int** myIntegerPointerPointer;myIntegerPointerPointer = &myIntegerPointer;
If we now dereference myIntegerPointerPointer once, we have
a pointer to an integer:
(*myIntegerPointerPointer) == myIntegerPointer
== memory address of myInteger
If we dereference it twice, we get the integer again:
(**myIntegerPointerPointer) == the thing at memory address
myIntegerPointer == myInteger
Creating an example program that demonstrates these equalities is an exercise for the reader.
Now that you know about memory and pointers, let's take a look at how we would declare variables to types other than integers. To start, assume that we have this simple class for the sake of later examples:
[Foo.H]
class Foo {
public:
Foo(); // default constructor
Foo(int a, int b); // another constructor
~Foo(); // destructor
void bar(); // random method
int blah; // random public instance variable
};
To declare a variable to this class and create the class in Java, you could say:
Foo myFooInstance = new Foo(0, 0);
The above is not valid C++ syntax. The new operator returns a pointer to
whatever follows it. The correct C++ syntax follows:
Foo* myFooInstance = new Foo(0, 0);
We have just made a pointer to an instance of type Foo and assigned it a
value, the address of the instance.
Now, let's call the method bar on the instance. In Java,
you would code:
myFooInstance.bar();In C++ you can't do this, since myFooInstance is a pointer,
and pointers need to be dereferenced before being used. To call a method
through a pointer in C++, you would code:
myFooInstance->bar(); // dereference the pointer and call the methodLikewise, we can access public instance variables of instances of
Foo. Of course, you would never do that. :)
myFooInstance->blah = 5;
The arrow operator -> does two things for you:
it dereferences the pointer, and then it calls a method on the instance or
accesses a member variable. This is shorthand for saying:
(*myFooInstance).bar();
which is basically carrying out the dereference and access steps individually. Since the arrow is shorthand for this, carrying out the two steps manually is almost never done.
In Java, the only way to create an object is to new one and
store a reference to it in a variable. In C++, it is possible to declare objects
without newing them explicitly. For example, here we declare a
local variable of type Foo without using new and a
pointer:
Foo myFooInstance(0, 0);
This line of code creates a variable of type Foo and passes the
specified parameters to its constructor. If we wanted to create a
Foo instance using the default constructor instead, we could
say:
Foo myFooInstance; // same as Foo myFooInstance();
In Java, myFooInstance would be a null reference. In C++, it's an actual
instance.
If we don't want to refer to the instance later, say, because it is being passed as a parameter, we can leave out the variable name:
// ... suppose the class Bar defines the method setAFoo(Foo foo) ...
Bar bar;
bar.setAFoo( Foo(5,3) ); // pass an instance of Foo
Calling methods and accessing public instance variables of an instance has the same syntax that you're used to in Java:
myFooInstance.bar();
myFooInstance.blah = 5;
Like pointers, instances may be local variables or member variables.
If an instance is a member variable of a class, its constructor can be called in the
class's constructor's initializer list, as in the following example:
[Bar.H]
#include "Foo.H" // must include Foo.H since we declare an instance of it
class Bar {
public:
Bar(int a, int b);
protected:
Foo m_foo; // declare an instance of Foo
};
[Bar.C]
Bar::Bar(int a, int b) : m_foo(a,b) // call Foo::Foo(int,int) and
initialize m_foo
{
Foo fooLocal; // create another instance of Foo, this time as a local var
// do something with the two Foos, m_foo and fooLocal
}
Suppose you allocate a chunk of memory for an object. Sometimes, it may be useful to refer to this block of memory with more than one name. We can sort of already do this with pointers, since multiple pointers can point to the same object. There is also a way to do it without using pointers; we can use something called references instead. Look at the program below to see how references can be used:
[main.C]
#include <iostream>
using namespace std;
int main(int argc, char **argv) {
int foo = 10;
int& bar = foo;
bar += 10;
cout << "foo is: " << foo << endl;
cout << "bar is: " << bar << endl;
foo = 5;
cout << "foo is: %d\n" << foo << endl;
cout << "bar is: %d\n" << bar << endl;
}
Here, we have allocated memory to hold an integer and named it foo
in the first line. The & sign you see in the second line
declares a reference to an integer variable. By assigning foo to
bar, bar does not become a copy of foo,
but instead refers to the same memory location as foo. When you
change the value of bar, it also changes the value of
foo and vice versa. References are essentially the same as
pointers, except that they are dereferenced like instances, can never be
NULL, and can only be assigned to once, at creation. The output of the above
program should look like:
foo is: 20
bar is: 20
foo is: 5
bar is: 5
Since references can be assigned to only at creation, references that are members of a class must be assigned to in the constructor's initializer list:
[Bar.H]
class Foo;
class Bar {
protected:
Foo & m_foo; // declare an reference to a bar
public:
Bar(Foo & fooToStore) : m_foo(fooToStore) {}
};
References are used most commonly when dealing with parameters; see the parameters section for more information.
You can convert between pointers and instances using the "*"
and "&" operators that were mentioned above.
Example 1 (making a pointer from a local variable):
Foo myFooInstance(0, 0); // create local variable instance of foo
Foo* fooPointer; // declare a pointer to Foo classes.
fooPointer = &myFooInstance; // set the value of the pointer to be the address
// of foo.
Now, the following two statements have the same effect:
myFooInstance.bar(); // call bar through the instance
fooPointer->bar(); // call bar through the pointer
Example 2 (making a local variable from a pointer):
Foo* fooPointer = new Foo(0, 0); // create a pointer to Foo and give it a
// value
Foo myFooInstance = *fooPointer; // dereference the pointer and assign it
// to myFooInstance; copy is made (!)
The above code may not have the result that you expect. If you
remember from earlier, we had an example of a
pointer to an integer that we dereferenced and stored in a second integer
variable. We discovered that the second integer was actually a copy of the
first. A similar thing is happening here: the instance that
fooPointer points to and myFooInstance are actually
two separate instances.
The first line news an instance and assigns the address of that
instance to the pointer. The second statement dereferences the pointer and
assigns the instance to myFooInstance.
Here, the compiler performs a bitwise copy of the instance pointed
to by fooPointer and assigns it to myFooInstance, or, if you have defined a
copy constructor, a copy of the class is created using that. So, saying
fooPointer->blah = 5; would not change the value of
blah in myFooInstance. Doing things like this yields
really confusing code and is a potential source of really big, juicy bugs. For
this reason, it is usually a bad idea to do this.
By the way, what is a copy constructor?
A copy constructor is a constructor that is invoked when one instance of a class is assigned to another instance, such as being copied when it's passed as a parameter.
The syntax for a copy constructor is:
class Foo {
Foo(const Foo &classToCopy); // copy constructor
};
A copy constructor usually assigns all the values of instance variables in the class that is passed in to the instance variables that this constructor is called on.
Clearly, declaring and using variables is a major aspect of programming. The memory needed to store these variables varies with the type of the variable and where it is declared. There are two major categories of storage:
The following block of code shows an integer and a instance of the class
Bar being allocated in local storage:
{
int myInteger; // memory for an integer allocated
// ... myInteger is used here ...
Bar bar; // memory for instance of class Bar allocated
// ... bar is used here ...
}
The { and } symbols mark the beginning and the end
of a block. When program flow enters the block, memory needed to store
an integer is allocated for myInteger, and memory needed to store
the class instance is allocated for the variable bar. When the end
of the block is reached, this memory used to store myInteger and
bar is freed up and those variables cease to exist. Trying
to use the variables after the block is closed will yield compile errors, just
as in Java.
new
In the example above, we're out of luck if we want to use bar
outside of its block. If we want to do this, we need to put bar
in global storage instead. In C++, we can request a block of memory in global
storage for certain data types by using new, and we return the
memory by using delete.
As you've seen in the pointers section, the syntax
for the new operator is as follows:
new ClassName(<initializer list>);
On success, a chunk of memory that is the size of the object is allocated
and a pointer to that memory is returned. If the memory can not be allocated
to store the instance, (which will most certainly never happen on our
machines,) it returns a value of 0, C++'s representation of
null. Note that if the class has a constructor that takes
no parameters, the parentheses and the initializer list are optional.
The following C++ code shows how you can allocate memory and use it later:
[Bar.H]
class Bar {
public:
Bar();
Bar(int a);
void myFunction(); // this method would be defined elsewhere (e.g. in Bar.C)
protected:
int m_a;
};
Bar::Bar
{
m_a = 0;
}
Bar::Bar(int a)
{
m_a = a;
}
[main.C]
#include "Bar.H"
int main(int argc, char *argv[])
{
// declare a pointer to Bar; no memory for a Bar instance is allocated now
// p currently points to garbage
Bar * p;
{
// create a new instance of the class Bar (*p)
// store pointer to this instance in p
p = new Bar();
if (p == 0) {
// memory allocation failed
return 1;
}
}
// since Bar is in global storage, we can still call methods on it
// this method call will be successful
p->myFunction();
}
Notice that you can still use the object generated by the new
statement even if you are outside the block. You can see that except for pointer
statements, this segment of code is the same as in Java. (Indeed, Java is
doing exactly the same thing behind the scenes.)
delete
In Java, you allocate memory for an object using new, and
a garbage collector frees the memory automatically when no existing object
references it. In C++, you have to be much more responsible than that.
Whatever memory you allocate in global storage, you must explicitly free, or
your program will swell in size and contain what are called
memory leaks. To avoid leaks, you need to keep track of all the
memory you have newed and free it when you no longer need it.
Actually, it is very easy to free memory that you have newed.
Use delete to deallocate the memory when you are done with it.
For example, to free the memory allocated above, add this line at the end of
the function:
delete p; // memory pointed to by p is deallocated
Remember that only objects created using new should be
deleted with delete! Instances created in local storage
are automatically recycled and should not be deleted explicitly. For
example, the following code will make your program crash:
Bar bar; // bar not created with new
// ... use the instance of Bar ...
delete bar; // EEK! bar is in local storage...program crashes!
Technically speaking, deleting objects is pretty easy. So, it seems, it should be just as easy to avoid leaking memory in your programs. Unfortunately, this is not the case; people often write code that has leaks everywhere. Later, in the debugging section, we will introduce some methods for eliminating leaks. However, there is an easy and effective way of avoiding them: good programming style.
We mentioned class destructors earlier, but we didn't mention their
use. In Java, you don't have to deallocate memory. You seldom need
to fill in the finalize() method for an object. In C++, memory
that is newed is not deallocated automatically, so you have to
explicitly free it. Since you can free memory at any time your program is
running, the question is when to do it. The following is a good rule of thumb:
memory allocated in a constructor should be deallocated in a destructor, and
memory allocated in a function should be deallocated before it exits.
The following C++ class definition is an example of poor memory management in a class:
[Foo.H]
#include "Bar.H"
class Foo {
private:
Bar* m_barPtr;
public:
Foo() {}
~Foo() {}
void funcA() {
m_barPtr = new Bar;
}
void funcB() {
// use object *m_barPtr
}
void funcC() {
// ...
delete m_barPtr;
}
};
Notice that in the above class, some memory is allocated when
funcA is called. This memory is freed up when the
function funcC is called.
Here is some code that uses the above class:
{
Foo myFoo; // create local instance of Foo
myFoo.funcA(); // memory for *m_barPtr is allocated
// ...
myFoo.funcB();
// ...
myFoo.funcB();
// ...
myFoo.funcC(); // memory for *m_barPtr is deallocated
}
The above code does not leak any memory. When funcA is called,
we allocate some memory that is used internally by myFoo. Calling
funcB then uses the memory. Finally, calling funcC
frees up the memory. Since we have deleted all newed memory, this
code contains no memory leaks.
Now, let's take a look at some code that uses the Foo class improperly:
{
Foo myFoo;
//...
myFoo.funcB(); // oops, bus error in funcB()
myFoo.funcA(); // memory for *m_barPtr is allocated
myFoo.funcA(); // memory leak, you lose track of the memory previously
// pointed to by m_barPtr when new instance stored
//...
myFoo.funcB();
} // memory leak! memory pointed to by m_barPtr in myFoo is never deallocated
The above snippet has a couple of errors. First of all, we call
funcB before calling funcA. This means that
the memory funcB operates on has not been allocated yet. This
will cause a bus error, and your program will crash since m_barPtr is pointing
to some random memory. Now assuming calling funcB first did not
cause a crash, we proceed to call funcA two times in a row. The
first time we call it, we allocate the memory and store it in a variable. The
second time we call it, we allocate a new chunk of memory and assign it to the
same variable again. This means that we have now lost the pointer to the
previously allocated block of memory and have no way of finding it again.
This causes a leak, since this memory can never be deallocated.
Now take a look at the class below, which uses a constructor and destructor correctly:
[Foo.H]
#include "Bar.H"
class Foo {
private:
Bar* m_barPtr;
public:
Foo();
~Foo();
void funcA() {}
void funcB() {
// use object *m_barPtr
}
void funcC() {
// ...
}
};
Foo::Foo()
{
m_barPtr = new Bar;
}
Foo::~Foo()
{
delete m_barPtr;
}
Memory is always allocated in the constructor at the time a
Foo object is allocated. The memory is automatically deleted when
myFoo is deleted or goes out of scope. Using the
constructor above, it is impossible not to allocate the memory before we call
funcB, nor is it possible to forget to delete the memory, since
the destructor is automatically called.
After learning about pointers, references, and instances, you may have been wondering when each type should be used in your programs. Unfortunately, there is no hard and fast rule. What is important is that you know how memory is managed for each.
When dealing with pointers, you explicitly newed a class
or something, tying up some memory. Unless you explicitly call
delete after you are done using that instance, it will tie up
memory until your program exits.
The memory where a local variable instance is stored, on the other hand, is automatically managed by the computer. When a local variable ever goes out of scope, the memory that it ties up will be freed up automatically. It doesn't matter if you have a pointer or reference to it somewhere; if it goes out of scope, it will be destroyed, and the pointers and references to it will point to nothing. Note that this is different from Java's garbage collection.
A reference can be thought of as just another name for the value to which it refers. Consequently, references are automatically managed by the computer, and you don't need to worry about deleting them or anything.
As you know, in Java parameters are passed by reference. When you pass a reference to an object in Java, you can change the actual object by calling methods on it or accessing its public instance variables. In C++, parameters can be passed either by reference or by value. Think of passing by value as passing a copy instead of the real thing.
Here's an example of passing by reference. We define the function
IncrementByTwo to take a reference to an integer. Since the
function has a reference, it can alter the integer that is passed in to it:
void IncrementByTwo(int & foo) { foo += 2; }
You can increment an integer variable by calling:
int bar = 0;
IncrementByTwo(bar);
The variable bar will now have been increased by two.
Now, let's define the same function, only this time we will pass by value:
void IncrementByTwo(int fooVal) { fooVal += 2; }
If we use the same code above, bar will still be 0 after
IncrementByTwo has been called. This is because the formal
parameter fooVal contains a copy of bar. So,
passing by value here will not give the result that we want.
A third way to pass fooVal is to instead pass a pointer to it.
In this example, we define IncrementByTwo to take a pointer to
the integer:
void IncrementByTwo(int* fooPtr) { *fooPtr += 2; }
We call the function by passing a pointer:
int bar = 0;
IncrementByTwo(&bar); // note the & sign; remember, we pass a pointer to bar
Since we passed a pointer to bar, it will be incremented by two
just as we wanted.
So the question remains, how do we pass around objects in C++? Well, just
as our integer bar above, objects can be passed by reference, by
value, or by passing a pointer to the object. Since objects are often
newed, meaning that you have a pointer to them, they are most
commonly passed by a pointer. Of course, they can be passed by reference as
well. However, objects are generally not passed by value, since that implies
that a copy of the object is being made. For small types like integers, making
a copy is not a big deal; but for objects, this can take up a lot of time. If
you're passing by value to make sure that the object you passed in won't be
changed, instead make the input parameter const. (See below for
a description of const parameters.)
Return values
Return values can be passed in all the ways discussed above. Discussing return values, however, allows us to note a common C++ pitfall: passing a local variable outside of its scope. Above, we mentioned that variables in local storage are automatically destroyed when the block they are in closes. So, if you return a pointer or reference to a variable declared in this manner, and the variable leaves scope at some time, the pointer or reference will point to trash.
To see this, look at the following example:
[FooFactory.C]
#include "FooFactory.H"
#include "Foo.H"
Foo* FooFactory::createBadFoo(int a, int b) {
Foo aLocalFooInstance(a,b); // creates an local instance of the class Foo
return &aLocalFooInstance; // returns a pointer to this instance
} // EEK! aLocalFooInstance leaves scope and is destroyed!
Here, we've created an instance of the Foo class, passing its
constructor the input parameters of the createBadFoo method.
We then get the memory address of this instance and return it.
At this point, everything is fine: we have a pointer to the instance of
Foo that we just created. At the next step, however, the function
ends since we returned, causing aLocalFooInstance to be destroyed.
Now, that pointer we returned is pointing to, well, garbage.
Note that this next example is flawed as well, since we return a reference to a local variable:
Foo& FooFactory::createBadFoo(int a, int b) {
Foo aLocalFooInstance(a,b); // creates an local instance of the class Foo
return aLocalFooInstance; // returns a reference to this instance
} // EEK! aLocalFooInstance leaves scope and is destroyed!
The solution to this problem is to either return a pointer to an instance in global storage, or to return an actual instance:
Foo* FooFactory::createFoo(int a, int b) {
return new Foo(a,b); // returns a pointer to an instance of Foo
}
Foo FooFactory::createFoo(int a, int b) {
return Foo(a,b); // returns an instance of Foo
}
The moral of the story: never return pointers to variables you did
not new, unless you can be completely sure that they will never
leave scope.
Basic Java types such as int, double,
char have C++ counterparts of the same name, but there are a few
differences:
string class is slightly different from
the Java String class in that in C++ an instance
of string can be modified after it is created.
null keyword. Instead,
NULL is defined to be the constant 0.
In C++, you can define enumerated types using the
enum keyword. Enumerated types are sometimes useful for
expressing a value that has a limited range. For example, we might create
an enum for the life cycle of a caterpillar:
enum CatLifeCycleType
{
LARVA,
CATERPILLAR,
PUPA,
BUTTERFLY
};
You can now create a variable of type CatLifeCycleType and
assign to it values such as LARVA or PUPA.
Useless fact: By default, the values declared in an enum
statement are assigned integer values starting at 0 and increasing. So, in
the above example, LARVA represents the number 0,
CATERPILLAR is number 1, and so on.
Useful fact: You may bypass this default numbering and assign each enumerated value an actual integral value. For example, redefining the above example:
enum CatLifeCycleType
{ LARVA = 1, CATERPILLAR = 2, PUPA = 3, BUTTERFLY = 4 };
Useful fact: An enumerated type can be cased off of in a switch
statement.
const keyword
In C++, the keyword const means different things according to its
context. When you add const in front of a variable, it means that
variable is treated like a constant. You will not be able to change
the value of a const variable once you assign it. An example of its
usage would be:
const float PI = 3.14156;
If an object is declared as const, then only the
const functions may be called. If const is used
with a member function, that means only const objects can call
that function. For example, suppose you have the following class:
[Foo.H]
class Foo
{
public:
void ChangeValue(int newVal) { m_val = newVal; }
int GetVal() const { return m_val; }
const float PI = 3.14156;
protected:
int m_val;
};
If an instance of foo is declared as const,
you cannot call ChangeValue on it. Correspondingly, since
GetVal is declared const it cannot modify
_val.
The const keyword can be used in another way that you might
not expect. Parameters in a function may be declared const, which
means that those parameters will not be changed during the function call. For
example, consider the following function:
int multiply(const int a, const int b) { return a*b; }
Now, does this mean that only constants can be passed into
multiply? No. Rather, it means that during this function,
the parameters a and b will be treated as constants.
There are several reasons why it is good practice to use const
whereever you can. One main reason is efficiency. When you pass an object by
value to a function, the program needs to allocate memory for that object and
make a copy of it. When you make the object a const parameter, it
will not create a new copy of that object. Repeated function calls with
const parameters are much faster then non-const
parameters.
For example:
is much faster than
float GetRed(const int x, const int y)
float GetRed(int x, int y)
Secondly, declaring parameters and/or functions as const
improves program readability. If you declare a function const,
for example, anyone reading your code will know right away that the
function doesn't change the object on which it is called. In addition, the
compiler will return an error if a const function modifies
its object, or if a const parameter is modified in its function.
People often make errors when they write numeric expressions. These are often the most notorious and most difficult bugs that can ever be present in your programs. Here are a few tips you should know:
doubles using ==. Their values
are not exact, so sometimes what you expect to be equal isn't.
doubles as parameters when calling math functions.
double to an int, Java will issue a
compile-time error. You must make such a cast explicit for the code to
compile. In C++, the implicit cast compiles without error.
int x = 5; int y = 2; double z = 5.0;
double a = x / y; // a equals 2
double b = z / y; // b equals 2.5
double c = (double)x / (double)y; // c equals 2.5
This can be a source of very annoying bugs. To avoid these "errors" from
occurring, explicitly cast an int to a double
before performing a computation, as is done for the third example above.
You can later cast back to an int if necessary.
int expressions,
multiply first before dividing (e.g., a * b * c / d / e; ).
floats and doubles:
double a ;
int b ;
double EPSILON = 1e-6;
// You want to equally divide a and execute loop b times
// instead use:
// for ( double i= 0 ; i < b; i+= a / b);
//
for (double i = 0 ; i < b - EPSILON; i += a / b);
The Standard Template Library (the STL) provides a way for you, the programmer, to use common data structures in an efficient and typesafe manner. It is important that you understand the rudiments of templates and templated classes in order to make use of the STL. A templated class only takes its final form at compile time. It uses parameters provided by the programmer to generate the templated code: in the STL, these parameters are usually type names. You've already seen other template patterns in mail-merges, spreadsheets, and mad-libs; just not in Java.
What is the advantage? In Java, data structures and other classes
typically take a java.lang.Object or
Comparable as paramaters in order to remain general.
This means that you lose type safety unless you spend the time to
write an adapter.
Using templates, types can be provided as parameters. The C++ compiler then uses the STL headers to generate code specific to the given types. For example,
[STLBasics.C]
// we will be using vectors, so we must include the standard vector header
#include <vector>
// we must use the standard ("std") namespace for the standard template library
using namespace std;
// ExampleClass will have nothing but a default constructor and destructor
class ExampleClass { };
// The main function where we will create and use STL vectors
int main(int argc, char **argv) {
// "intvector" is a vector where each element must be an int
vector<int> intvector;
// The compiler has now automatically created a vector class
// "intvector" that only takes type "int"
// "examples" is a vector where each element must be an ExampleClass
vector<ExampleClass> examples;
// The compiler has now automatically created a vector class
// "examples" that only takes type "ExampleClass"
// these two assignments will work perfectly:
intvector[0] = 6;
examples[0] = ExampleClass();
// these two will fail at compile time, which is preferable to failing
// at run time:
intvector[1] = ExampleClass();
examples[1] = 8;
}
Now we have type safety and speed, all in one system. Templates are one of the most advanced and powerful features of C++; after you gain familiarity with the STL, you may wish to write your own templated classes when appropriate.
STL Documentation online! Right here at SGI.
Complete documentation for STL Lists
An STL list is just a simple linked list. Please refer to the SGI link for more detailed information. There are a few syntactical concepts we need to formally introduce at this point.
list<char> mycharlist;". The template parameter
(in the case of a list) can be any type at all, so we could also have
a list of pointers (this is common): "list<MyObject*>
objectlist;"
list<MyObject*>
objectlist;", then if we called objectlist.front(), we would
get a MyObject* back. Likewise, if we had
"list<char> mycharlist;", calling mycharlist.front() will
return a char.
[MyListHeaders.H]
// we need to include "list" in order to create STL lists of our own
#include <list>
class Bar
{
// (your class goes here)
public:
virtual void printBar() const;
};
class Foo
{
public:
Foo();
virtual ~Foo();
// this will add a Bar* to the end of the list, and we now
// assume that the Bar* belongs to this class. (so we will delete
// it when the time comes)
virtual void addBarToList(Bar *newbar);
// this will get the first Bar* from the list
virtual Bar* getFirstBar() const;
// this will print out all of the Bar objects in the list
virtual void printAllBars() const;
protected:
// m_barList is a "list" with a template parameter "Bar*". This is
// the format used to declare templated variables.
list<Bar*> m_barList;
};
[MyListDefinitions.C]
// get all of the declarations above
#include "MyListHeaders.H"
// iostream has the "cout" stream that we use to print to the terminal
#include <iostream>
// all of the STL (and "cout") is in the "std" namespace. See the
// Stroustrup book for more information on namespaces.
using namespace std;
// (your Bar definitions could go here)
void
Bar::printBar() const
{
// your bar printout code here
}
Foo::Foo()
{
// nothing needs to be done -- list initialization is automatic
}
void
Foo::addBarToList(Bar *newbar)
{
m_barList.push_back(newbar);
}
Bar *
Foo::getFirstBar() const
{
// this will return the front element of the list
return m_barList.front();
}
void
Foo::printAllBars() const
{
// Please see the description below this code snippet to
// understand this loop.
for (list<Bar*>::const_iterator barIter = m_barList.begin();
barIter != m_barList.end();
barIter++)
{
// const iterators return "const Bar*"s
const Bar *currbar = *barIter;
// print out the current Bar*. This would generate
// a compile warning if "printBar()" was not a const
// method
currbar->printBar();
}
}
Foo::~Foo()
{
// Be careful about memory management here -- if anybody else uses
// these Bar*s there will be big problems!
// See the printAllBars method to better understand this for loop.
// Since we need to modify the Bar*s, we can't use a const_iterator
// this time.
for (list<Bar*>::iterator barIter = m_barList.begin();
barIter != m_barList.end();
barIter++)
{
delete (*barIter);
}
// often times we want to clear the list in the destructor. (in this case
// it would happen automatically)
m_barList.clear();
}
// the main function demonstrates how to use the class we've defined above
int
main(int argc, char **argv)
{
Foo f;
// Add a bunch of Bar*s to the Foo class. If we kept references
// to these Bar*s, we would be affecting the same objects that the
// list refers to, of course. (Be sure you understand why!)
f.addBarToList(new Bar());
f.addBarToList(new Bar());
f.addBarToList(new Bar());
// Of course, we might want to do something to the Bar* before passing
// it to the Foo:
Bar *anotherbar = new Bar();
// (Manipulate anotherbar here)
f.addBarToList(anotherbar);
// get the first and last bars in the list
Bar *firstbar = f.getFirstBar();
Bar *lastbar = f.getLastBar();
// print out the first Bar
cout << "First Bar:" << endl;
firstbar->printBar();
// print out the last Bar
cout << "Last Bar:" << endl;
lastbar->printBar();
// print out all of the Bars
f.printAllBars();
// f's destructor will be called when it goes out of scope
}
|
So, what's going on in the Foo::printAllBars() method? Let's look at the for loop in detail; if you understand this, the STL is almost under your control. It is worth your time to study this method.
|
|
Complete documentation for STL Deques
A Deque is essentially the same thing as the java.util.Vector you may already be familiar with. It does have some other features worth noting, namely O(1) front insertion. There is also an STL Vector, and its functionality is nearly identical.
To save space and confusion, you'll only see snippets of code here; for complete examples see the repository in /course/cs032.
#include <deque>
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
// initialize the deque with a size of 10
deque<int> intdeque(10);
for (int count=0; count<10; count++) {
// "intdeque.at(index)" is equivalent to the "my_array[index]" operator in java
intdeque.at(count) = count*10;
cout << "intdeque.at(" << count << ") = " << intdeque.at(count) << endl;
}
// make the deque bigger
intdeque.resize(100);
for (int count=90; count<100; count++) {
intdeque.at(count) = count*10;
cout << "intdeque.at(" << count << ") = " << intdeque.at(count) << endl;
}
// push a few elements on the front
intdeque.push_front(32);
intdeque.push_front(2002);
intdeque.push_front(314159);
// print out the first five elements again
for (int count=0; count<5; count++) {
cout << "element " << count << " is " << intdeque.at(count) << endl;
}
}
Complete documentation for STL Strings
Strings in the STL are similar to strings in Java. They have comparable properties and features, and they have handy internal reference counting and efficient copy constructors (Don't worry if you aren't familiar with these terms). The biggest single difference is the mutability of strings in the STL; Java strings cannot be modified after creation, whereas STL strings can be modified and even expanded.
C++ strings replace the "char*"s of C. A conventional char* is an array of characters, terminated by a zero character. For instance,
const char *test = "Testing\n"
| Index: | test[0] | test[1] | test[2] | test[3] | test[4] | test[5] | test[6] | test[7] | test[8] |
| Character: | 'T' | 'e' | 's' | 't' | 'i' | 'n' | 'g' | '\n' | 0 |
Note that the terminating 0 is not the ascii character ('0'), but the actual int (0). The ascii escape code for 0 is '\0'. Ask a TA for more information about char*s generally, as this tutorial is determined to leave them behind. If you're using an STL string but you need a C-style char*, then use the "c_str()" method of the string class.
string mystr("Testing C++ -> C strings");
const char *my_c_string = mystr.c_str();
Here are some examples of how to use strings:
#include <string>
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
// make a new string
string s1("new string.");
// make another new string, but in a different way
string s2 = string("another string.");
// you can append one string with another using the "+" operator
string s3 = s1 + s2;
// the compiler will automatically make an STL string out of the
// literal string below.
string s4 = s1+" More appended text";
// output all the strings
cout << "s1: " << s1 << endl;
cout << "s2: " << s2 << endl;
cout << "s3: " << s3 << endl;
cout << "s4: " << s4 << endl;
// we can use the "at()" method as we did with the deque
cout << "s4.at(4) = " << s4.at(4) << endl;
s4.at(4) = 'Z';
cout << "Modified s4.at(4). Now s4.at(4) = " << s4.at(4) << endl;
}
The output of the above code is
s1: new string.
s2: another string.
s3: new string.another string.
s4: new string. More appended text
s4.at(4) = s
Modified s4[4]. Now s4.at(4) = Z
STL strings are often passed by copy. STL strings are one of the few STL structures where copying is even "acceptable." (There are exceptions to all of these rules, of course, but generally you should make sure you know what you're doing before you copy STL structures) STL strings are internally reference-counted and only copy the data upon modification. Usually STL objects are passed by reference in order to ensure that caller and callee are using the same data structure.
Complete documentation for STL Maps
If you were asleep in your data structures class, a map is a structure that associates a key with a value. Maps in the STL allow for the efficient retrieval of values through the use of things like balanced binary search trees (which are conveniently hidden from view).
Maps use something called a "pair" to store their data. A pair is a templated class that has two members, "first" and "second". The map stores key/value pairs, and so the key is "first" and the value is "second." Is this confusing? A little bit, yes, but it greatly simplifies iterative tasks. (The next example illustrates this convenience)
The maps have a lot of features, but most programmers only use a few:
(*myiter).first", and to get the value we would use
"(*myiter).second". If myiter==mymap.end(),
then it's invalid. Usually this means we failed a search, or we have
reached the end of the structure.
[MapExample.C]
#include <map>
#include <string>
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
// the map "phonebook" has key type "string" and value type "int"
map<string,int> phonebook;
// make an entry for the sun lab
phonebook["Sun Lab"] = 8637721;
// make an entry for the graphics lab
phonebook["Graphics Lab"] = 8637693;
// make an entry for the TA Room. This syntax is harder to read but
// doesn't make use of any potentially confusing or misleading operator
// overloading like the last two. Use either version for insertion.
phonebook.insert(pair<string,int>("TA Room",8637720));
cout << "--> Printing out all elements in alphabetical order:" << endl;
// iterate through all of the elements in the map
for (map<string,int>::const_iterator iter = phonebook.begin();
iter != phonebook.end();
iter++) {
// grab the key and value from the iterator
string key = (*iter).first;
int value = (*iter).second;
// print out the key
cout << "Key: \"" << key << "\"";
// print out the value
cout << ", Value: " << value << endl;
}
cout << "--> Completed element printout." << endl << endl;
// try to find the entry for the Sun Lab. If there is no entry,
// find returns phonebook.end().
if (phonebook.find("Sun Lab") == phonebook.end())
{
cout << "ERROR! \"Sun Lab\" not in phonebook." << endl;
exit(-1);
} else {
cout << "Test Passed: \"Sun Lab\" is in phonebook." << endl;
}
// try to find the entry for the Systems Lab
if (phonebook.find("Systems Lab") == phonebook.end())
{
cout << "Test Passed: \"Systems Lab\" not in phonebook." << endl;
} else {
cout << "ERROR! \"Systems Lab\" is in phonebook." << endl;
exit(-1);
}
// remove the entry for the Sun Lab, since we know it's present
// from above
phonebook.erase("Sun Lab");
// ( note that you can also pass an iterator to erase() )
// check if the Sun Lab has been successfully removed
cout << "(Attempt has been made to remove \"Sun Lab\")" << endl;
if (phonebook.find("Sun Lab") == phonebook.end())
{
cout << "Test Passed: \"Sun Lab\" not in phonebook." << endl;
} else {
cout << "ERROR! \"Sun Lab\" is in phonebook." << endl;
exit(-1);
}
}
Using the STL takes a bit of getting used to. Even once you understand the syntax and semantics, though, you need to be smart about memory management. Here are a few basic guidelines. There are exceptions to all of these rules, but most of the time they're a good place to start from.
deque<int>,
vector<double>, map<char,long>
delete operator. Establishing clear ownership
policies will ensure that memory leaks (and accessing
previously-deleted memory) don't hamper the development of a project.
If you have taken ownership of an object pointer in
an STL structure, make sure you remember to delete it. STL structures
are a common source of difficult memory leaks. deque<MyObject*>,
vector<ObserverObject*>, map<int, Worker*>
map<string, Worker*>, deque<string>,
map<int, string>
Flow of control constructs are very similar in Java and in C++, with the
difference being that Java has a strong bool type and C++
does not. In C++ conditional expressions (such as the expression
that decides which branch to take in an if statement) are cast to
integers. The counterparts of true and false
in C++ are any expressions which evaluate to non-zero and zero values.
This means that the type checker will not catch many typos you might
make. For instance a common mistake is:
if (a = 3) {
// do something
}
Here an assignment "=" was used instead of an equality comparison
"==". For this reason it is often useful to put constants on
the left when possible and to double check your conditional expressions.
The if statement
The syntax for this statement is the same in C++ and Java:
if(<predicate>)
<do this if predicate is true (!=0)>
else
<do this if predicate is false (==0)>
As in Java, the else block is optional.
The switch statement
The switch statement is the same as in Java also.
switch (<variable to case on>) {
case <value 1>:
<stuff to do if above variable == value 1>
break;
case <value 2>:
<stuff to do if above variable == value 2>
break;
case <value 3>:
<stuff to do if above variable == value 3>
break;
default:
<stuff to do if none of the cases matches the variable>
break;
}
The variable or expression that the switch statement cases on
can be of any type whose equality can be tested using ==.
Integers and enumerated types are two commonly
used values in a switch statement.
In switch statements, when a case section is
done executing, flow of control will fall through into the case below.
To avoid this, always put a break statement at the end
of each case, and save yourself confusion later. If you must have a
case block fall through for some reason, make sure you
comment it so you (and others) know it is intentional.
Like Java, you do not have to have a default case.
Boolean expressions
A predicate is any boolean expression, i.e. an expression that
evaluates 0 for false, or non-zero for true. As far as syntax goes, C++ and
Java predicates are identical. Just be sure to remember that zero
means false and non-zero means true.
You can combine boolean functions in any order to generate predicates.
Listed below are several boolean functions that you should know. The functions
are listed in decreasing order of precendence, and "false" really
means zero and "true" means non-zero:
!x Returns false if x is true and vice-versa.
x < y Returns true if x is less than y, else false.
x > y Returns true if x is greater than y, else false.
x <= y Returns true if x is less than or equal to y, else false.
x >= y Returns true if x is greater than or equal to y, else false.
x == y Returns true if x and y are equal, else false.
x != y Returns true if x and y are not equal, else false.
x && y Returns true only if both x and y are true.
x ^^ y Returns true if either x or y is true (not both)
x || y Returns true if one of x or y is true (or both)
All these operators can be combined in any way you want to generate complex
expressions. Since it is easy to forget the precendence rules, always
use parentheses in your expression to make it easier to read and to be sure
that it does what you want.
Remember that testing for equality uses ==, not =. In Java, if
you accidentally tried to test if two things are equal using =, you
would get a compile-time error. In C++, you will not get a compiler error and
your program will just behave unexpectedly! This mistake is very easy to make
and hard to track down, so be as diligent as you can to avoid it.
9. Iteration
Loops in Java and in C++ are practically identical. Here is a list of
different types and syntaxes:
The for loop
Syntax:
for(<initialize counters>; <loop condition>; <increment counters>)
<statement>
For example:
for(int i = 0; i < 10; i++)
cout << "I am counting to 10" << endl;
The loop body gets executed while the loop condition is true, and the loop
terminates the first time it is false.
It should be noted that in some C++ compilers, including some on our system,
the scope for a counter declared in the for loop definition is considered to be
outside the loop. For this reason, the following code is valid in Java
but could produce "Multiple variable declaration" compile errors in C++:
for(int i = 0; i < 10; i++) {
// do something...
}
for(int i = 0; i < 10; i++) {
// oops, counter i already declared...possible C++ compile error...
// to correct, change this loop to read "for (i = 0; ...", or declare
// i at start of function to avoid ambiguity
}
The while loop
Syntax:
while(<expression>)
<statement>
The statement gets executed so long as the expression in the
parentheses evaluates to true (non-zero). This is just like a for
loop without a counter variable:
for(;<expression>;)
<statement>
The do...while loop
Syntax:
do
<statement>
while(<expression>);
In a do...while loop, the statement is executed before the
expression is evaluated, so the statement is executed at least once, even if
the expression is false. This differs from a while loop or a for
loop in this respect.
10. The Command Line
As you already know, you can pass in command line arguments to a
program when you execute it in a shell. Many shell commands take
in command line arguments. One example is ls -l. Here, we are
executing the program ls and passing it the command line argument
-l.
How does a program read in command line arguments? Take a look at the
following code:
[main.C]
#include <iostream>
using namespace std;
int main (int argc, char* argv[])
{
cout << "Total number of arguments: " << argc << endl;
cout << "Your executable name: " << argv[0] << endl;
for (int i = 1; i < argc; i++) {
cout << "Argument # " << i << ": " << argv[i] << endl;
}
}
The program will print out all the command line arguments passed to it.
Notice how argc is used to control the number of strings
(char*s) we read from argv. argv[0]
will always be the name of the executable itself! argv[1]
is the first command line argument, argv[2] is the second one,
etc.
If you compiled this program and named the executable my_exec,
a sample execution could produce the following output:
$ my_exec testa testb
Total number of arguments: 3
Your executable name: my_exec
Argument # 1: testa
Argument # 2: testb
$
Often times, you want to input numbers as command line arguments. However,
all arguments are read in as strings. To convert strings to numbers
the easiest thing to do is to use the stringstream class.
Look at the int2string.C file in the strings repository.
As we saw in the Hello World program,
the main function returns an integer in C++. In this program
we return 0 (implicitly) since the program flow reached the end of the
main function successfully.
11. The Preprocessor
Before the "real" compiler actually touches your program, a program
called the preprocessor processes it. The job of the preprocessor is
to do simple text substitutions and the like. Preprocessor commands
all start with the # character. While many C programs
have relied heavily on use of the preprocessor, one of C++'s goals
has been to eliminate most preprocessor use. However, there are still a few
things that you must know.
#include statements
The preprocessor allows you to include the contents of one file in
another. This is performed using the #include statement. If the
name of the file to be included is enclosed in angle brackets <
>, the compiler will search a standard list of directories
for the file you wish to include; you can add entries to this list with
the compiler flag -I. On the other hand, if the name
of the file is in quotes " ", it will search the current
directory for the files in addition to the other directories.
For example:
#include <math.h>
#include <iostream>
#include "MyHeader.H"
The #include directive is typically used to load header
files. The following is a non-exhaustive list of times when you will need
to include the header file for a class:
- when you create an instance of the class
- when you call a member function on an instance of the class
- when you access a public instance variable on an instance of the class
- when you access a static function or member of the class
- when you define methods for the class (i.e.
Foo.C must include Foo.H)
- when you declare a member instance in the class declaration
- when the class you're declaring is a subclass of the class (i.e.
SubClass.H must include SuperClass.H)
Including lots of header files in a header file is discouraged. See
the section on forward declarations for more
information.
#define statements
The #define preprocessor directive will tell the preprocessor
to do text substitution. For example, the following directive will substitute
every occurrence of FIVE with the number 5:
#define FIVE 5
The #define statement can also do substitution
with parameters, allowing you to write simple macros. However, using
#define macro substitutions can have many negative
side effects. Since C++ has features that make using macros largely
unnecessary, we advise that you avoid them. To declare
constants, you could declare an extern const int in some header
file and assign that int a value in some program file.
To declare short functions, simply write inline code.
A macro can be undefined with the #undef directive.
Conditional compilation
You can use the preprocessor to conditionally compile code. The
statements used to do this are #if, #ifdef,
or #ifndef as well as a #endif following the
conditionally compiled section. For example:
#define COMPILE_SECTION
// ...
#ifdef COMPILE_SECTION
// some code here
// this code would be compiled since COMPILE_SECTION is defined above
#endif
Conditional compilation can be used for avoiding circular includes (see
below), writing code for multiple platforms, or optionally showing
debugging messages.
Circular includes
In general, things can't be defined twice in C++. This means that two
files cannot include each other. For example, if Foo.H
does a #include "Bar.H" then Bar.H cannot
#include "Foo.H". Even if the restriction on multiple
definitions didn't exist, this would clearly lead to infinite recursion.
Luckily, we can use conditional compilation to avoid this. All of
your header files should have something like this:
[Foo.H]
#ifndef Foo_Header
#define Foo_Header
// put all of the header file in here!!
// remember this endif or you will have big, big problems!
#endif
The first time the file is read, Foo_Header isn't defined, so it
defines it and reads the code in the header file. The second time,
Foo_Header will already be defined, so your code will be skipped,
making your class declaration defined only once.
Forward declarations
You've now learned how to prevent circular includes, but you might be
wondering how to deal with two classes that actually need to know about
each other. It turns out that you don't need to know anything about
a class other than its name in order to declare a pointer to it.
So, if a class contains a pointer to another class, instead of
including the first class' header in the second's header, simply
make a forward declaration. Then, include the header in the .C
file:
[Foo.H]
#ifndef _FOO_H_ALREADY_INCLUDED_
#define _FOO_H_ALREADY_INCLUDED_
class Bar; // forward declaration; says "class Bar exists, but we
// don't know anything about it"
class Foo {
// ...
protected:
Bar* m_bar; // We can declare a pointer to a bar. We can't call
// any methods or declare a non-pointer bar until we
// include its header file.
};
#endif
[Foo.C]
#include "Foo.H"
#include "Bar.H" // must include here if we want to instantiate a Bar
// ...
In fact, forward declarations should not just be used to avoid circular
includes. Using forward declarations instead of #include
statements in your header files can drastically decrease the time it
takes to compile your program after you change it. For this reason, use forward
declarations as much as possible and avoid including header files
unnecessarily. Your header files should have mainly forward declarations,
and your program files should have mainly #include
statements.
12. Build Process
This is just a short introduction to the entire build process of how your
code goes from your .C and .H files into an executable. In Java, you simply run
javac, and your .java files become .class files and you run your program using
the Java Virtual Machine which handles most of what you must do manually in C++.
This is why we use the utility called make, because it handles all
of this build process for you. For more information, see CS31 or CS167.
- For each .C file in your project, the preprocessor copies in all
the .H files that are
#included and creates a very
large temporary file.
- Then, each expanded temporary file (from the preprocessor) is
compiled into an object (.o) file, which contains the methods and
data in the .C file in a format that can be executed. There is also
a table of symbols (variables and functions) that are referenced but
not defined in this particular .o file. (e.g. C Library functions,
stuff from the support code, et cetera)
- In order to resolve all of these symbol references, the final
step is to link the .o files together into an executable. The
linker needs all of the .o files and any external libraries
that have any referenced but unresolved symbols.
The executable is basically a concatenation of your .o files with
some information about external libraries.
- When you run your program, any external libraries that were linked
in dynamically will be (you guessed it) dynamically loaded by the solaris
loader. Now your program can execute without issue.
- So, you need to find out which files need to be recompiled, build them,
then rebuild your linked object (executable) every time you make any modifications
to source. To make this easier, we use
make and the associated
Makefile to automate the process.
13. Debugging Tips
There are several kinds of errors you can encounter in your code:
- compile errors
- run time errors
- numerical errors
- algorithmic errors
- memory leaks
Compile errors
Compile errors are the easiest problems to tackle. Most of the time, they
are typos or obvious mistakes such as passing the incorrect numbers of
parameters to functions. More so than with Java, it's likely that each
actual error in your code will cause several compile errors to be
returned, possibly in multiple files. As you learn to program in C++
you'll learn what the compile errors are really telling you. In the
beginning, solve your errors one at a time and try to
compile again.
Run time errors
Run time errors can have many causes, the most notable (and
obvious) among these are the ones which crash your program. Often times,
stepping through your code manually is the quickest way to find these
bugs. Failing that, a powerful debugger called dbx is at your disposal.
dbx allows you, among many other things, to set breakpoints, to examine
values of variables, and to check memory access. However, dbx is a
cumbersome tool (especially for beginners) and may not be necessary to
track down a run time error. If you think you know what part of your code
caused the error, it might save you time by just checking the code
directly.
Numerical errors
Numerical errors are caused by limitations of your
underlying software and hardware implementation. For example, you can not
have a char value greater than 255, and there is limited accuracy for
floating point numbers. There is no way to eliminate a numerical
error. However, you can anticipate the range of your numerical
calculations and choose the proper variable types and algorithms.
Numerical errors are very difficult to track down. The best approach for
avoiding numerical errors is careful and incremental coding.
Algorithmic errors
Often, students spend time debugging their code over and over with out
thinking, "Is it my code which is wrong or my algorithm?" If you have
spent three hours debugging ten lines of code, most likely, the problem is the
latter. On such occasions, stop coding! Go home, eat some food, take
a shower, think about what could be going wrong with your code. Most times,
you will shout out "Eureka!", like Archimedes, and run back to the
Sun Lab to finish your program in 10 minutes. If you cannot figure out the
problem, don't be afraid to ask a TA. We don't encourage people to come with
a piece of code for us to debug at TA hours, but if you really put some
serious thinking into the problem and still cannot figure it out come ask a
TA on hours.
Memory leaks
Finally, when your program is just about complete, you should try to
get rid of any memory leaks in your code. Memory leaks occur when you
allocate memory with new that you fail to deallocate before
the program quits. To find out if you have leaky code, you can use the
bcheck utility. In a shell, type bcheck
<executable_name> <program_arguments>. This will output
a file named <executable_name>.errs that will contain
a list of all your leaks, along with the file names and line numbers where
they occured. (Memory leaks can be detected from dbx as well;
in fact, bcheck is just a wrapper around dbx
used for checking memory leaks.)
Additional information
The CS32 web site also has a collection of C++ resources and tools that are useful, including a list of common C++ compile errors.
The Answerbook has a section on dbx
that can help you get started.
14. Miscellaneous Tips
This is just a collection of miscellaneous pieces of advice, amalgmated from
students and TAs past and present about the use of C++ and things to keep in
mind in general. Some of this may be repetitive, but on the other hand it's in
an easy to reference location. If you have any suggestions on other things to
add to this list, please let us know.
- Whenever possible, when using references as parameters, use const
references - this will help prevent you from modifying objects that you don't
intend to modify, and will help you catch bugs in certain situations, like using
= incorrectly and accidentally copying some object.
- Do not forget to declare a method you intend to overwrite
virtual.
- Initialize all pointers that aren't being
newed immediately
to NULL. It can save a lot of headaches in debugging. Also, after deleting
a pointer, set the pointer to NULL for the same reason.
- When instantiating a local variable of type
Foo, if
Foo takes no arguments, then saying:
Foo myFoo(); is a syntax error. Do not use the parentheses.
You will get strange compile errors if you do.
- If all else fails, try doing a
make clean; make.
15. Makefiles
This is far too large a topic for the scope of a tutorial, make
is a large and evil beast. Below, you will find a sample CS123 Makefile with
the sections you can edit yourself marked. For more information on the make
utility, see the Answerbook.
[Makefile]
Don't worry about this, just leave it.
.SUFFIXES: .H .C .lex .y
This is the name of your executable
EXECUTABLE = brush
These are the names of your objects (if you add a .C
file, you'll need to add something here, which is namely the name of the .C
file without the .C - for example, if you add MyFile.C, you need to add MyFile
to this line. If MyFile only has a header file (MyFile.H), you do not need to
add anything here.)
OBJECTS = main_brush
DEBUGFLAGS = -g0 -xildoff
FASTFLAGS = -fast -unroll=6
Pick the line you want for speed or debugging.
COMPILEFLAGS = $(DEBUGFLAGS)
#COMPILEFLAGS = $(FASTFLAGS)
Only one of the lines above should be uncommented at a
time. The '#' symbol is a comment in Makefile-speak.
The three lines below tell make where to find the
C++ compiler:
SUNPATH = /opt/SUNWspro6.0/WS6
SUNINC = $(SUNPATH)/include/CC
CCC = $(SUNPATH)/bin/CC
Don't touch this.
MAKEDEP = makedepend
SUPPLOC = /course/cs123/lib/I
OFILES = $(OBJECTS:%=%.o)
This line tells the compiler where to look for files
specified with #include<file>
IFLAGS = -I. -I$(SUNINC) -I/cs/include/motif -I/usr/openwin/include
-I$(SUPPLOC)
This line specifies where to look for libraries
LFLAGS = -L$(SUPPLOC) -R$(SUPPLOC)
Do not touch anything below here.
all : $(EXECUTABLE)
$(EXECUTABLE) : $(OFILES)
@echo
$(CCC) -o $(EXECUTABLE) $(COMPILEFLAGS) $(OFILES) $(IFLAGS) $(LFLAGS) $(LIBS)
@echo " make finished at `date`"
%.o: %.C
$(CCC) $(COMPILEFLAGS) $(IFLAGS) -c $<
%.C: %.H
tidy:
$(RM) $(OFILES)
$(RM) *.*~ \#*\#
clean: tidy
$(RM) -rf Templates.DB/Modules.DB
$(RM) $(EXECUTABLE) Makefile.bak core Templates.DB/*
$(RM) ir.out mon.out core $(EXECUTABLE).errs .make.state
depend:
$(MAKEDEP) -- $(CFLAGS) $(IFLAGS) -- $(OBJECTS:%=%.C)
# DO NOT DELETE -- MAKEDEPEND needs this line. dude.
home
Questions? Comments? Arguments about C++?
Mail the
CS032 TAs.
Last modified: Thu Sep 3 03:20:09 EDT 1998