#include <string> using namespace std;You will find this class very handy, and you should learn how to use it.
A string is like a (char *) that has been made into a nice safe C++ class. I have some simple example code in the file hw2.cpp. Here's the beginning.
#include <iostream>
#include <stdio.h >
#include <string >
using namespace std;
main()
{
string str1, str2;
const char *s2;
char *s3;
As you see, I've declared two strings as local variables.
You can assign values to a string in one of many ways. For example,
you can simply assign it to a standard null-terminated C string:
str1 = "Hello World";If you want to print a string with printf(), you need to convert it to a char *. The member function c_str() does this for you. You can also get the string's length with the length() member function. For example, this next code will print out str1's length, and its string.
printf("%d %s\n", str1.length(), str1.c_str());
You can't print a string using printf() without using
c_str(). In other words, the compiler will yell at you if
you try printf("%s\n", str1);.
However, you don't have to use c_str() when you use cout. So, the following works for cout:
cout << str1.length() << " " << str1 << "\n";
You can also assign a string from a second string. For example, the next two lines of hw2.cpp copy str1 into str2 and then prints out str2 (which will be "Hello World"):
str2 = str1; cout << str2 << "\n";Now, this makes a copy of str1. If I modify str2, str1 will remain unaffected. You can access and modify individual characters of a string by treating them like char *'s. This code changes str1 to "Jello World". You see that str2 will be unaffected:
str1[0] = 'J'; cout << str1 << "\n" << str2 << "\n";Now, we can also turn a string into a (char *) and assign that to a variable. This will be an actual pointer to the bytes of the string, so in theory, were you to modify the (char *), the string will be modified. However, strict C++ compilers will only let you set them to const's, which cannot be modified.
s2 = str2.c_str(); cout << s2 << " " << str2 << "\n";
Moving on -- the next three lines show that you can assign a string from a (char *) variable, rather than a constant. And again it makes a copy. When I change str2, s3 remains unaffected:
s3 = "Daffy Duck"; str2 = s3; str2[0] = 'T'; cout << s3 << " " << str2 << "\n";Something else to note -- when we set str2 to be s3, the old value of str2 went away. Is that a memory leak? As it turns out, no, it is not -- when you declare a class as a local variable like that (or even as a global), the compiler/runtime system will make sure that any spare memory that it has used is freed up. That's nice.
Now, you can use the + operator to concatenate strings, and the += operator to append to a string. This is an example of operator overloading in C++. In my mind, it's an evil thing, but it is pervasive with strings, so get used to it. For example, the following code turns str2 into "Tim Plank Jello World":
str2 += " "; str2 += str1; cout << str2 << "\n"; }And that's the end of hw2.cpp. Make sure you go over every line of this code until you understand the output:
UNIX> hw2 11 Hello World 11 Hello World Hello World Jello World Hello World Hello World Hello World Daffy Duck Taffy Duck Taffy Duck Jello World UNIX>
Let's look at some code. In bert.h we define a new class called a Bert. It's nothing exciting, but will help in illuminating what goes on with memory, constructors and destructors:
#include <string>
using namespace std;
class Bert {
public:
Bert(char *);
~Bert();
void add_bert();
string getString();
int getLength();
protected:
string str;
};
|
In bert.cpp we implement Bert. You must specify an initial string that gets copied into str when you construct a Bert We print a message when the constructor and destructor methods are called. getString() returns str, getLength() returns the length of the string, and add_bert appends "bert" to str. Pretty simple:
Bert::Bert(char *s)
{
printf("Creating new Bert (%s)\n", s);
str = s;
}
Bert::~Bert()
{
printf("Bert destructor called (%s)\n", str.c_str());
}
void Bert::add_bert()
{
str += "bert";
}
string Bert::getString()
{
return str;
}
int Bert::getLength()
{
return str.length();
}
Ok -- now let's write a few pieces of code to test this out. First, here's
berttest1.cpp:
#include < stdio.h >
#include "bert.h"
main()
{
Bert s1("Dog"), s2("Rat");
s1.add_bert();
s2.add_bert();
printf("S1: %s\n", s1.getString().c_str());
}
One quick thing to note -- you can call a method on a return value of
a method call, as in s1.getString().c_str(). Now, when this
executes, we get:
UNIX> berttest1 Creating new Bert (Dog) Creating new Bert (Rat) S1: Dogbert Bert destructor called (Ratbert) Bert destructor called (Dogbert) UNIX>Note, the constructors for both s1 and s2 are called. Then the printf() statement is executed. Then the destructors for s1 and s2 are called when main() exits.
So, constructors are called for all local variables when the procedure is entered. And destructors are called when the procedure exits.
Berttest2.cpp is simple as well with a few procedure calls. You should be able to trace through this one easily.
#include < stdio.h >
#include "bert.h"
void a()
{
Bert s2("Rat");
s2.add_bert();
printf("S2: %s\n", s2.getString().c_str());
}
int main()
{
Bert s1("Dog");
a();
printf("S1: %s\n", s1.getString().c_str());
a();
return 0;
}
UNIX> berttest2
Creating new Bert (Dog)
Creating new Bert (Rat)
S2: Ratbert
Bert destructor called (Ratbert)
S1: Dog
Creating new Bert (Rat)
S2: Ratbert
Bert destructor called (Ratbert)
Bert destructor called (Dog)
UNIX>
Ok, now look at
berttest3.cpp:
#include < stdio.h >
#include "bert.h"
main()
{
Bert s1("Dog"), s2("Rat");
s1.add_bert();
printf("S1: %s\n", s1.getString().c_str());
s2 = s1;
s1.add_bert();
printf("S1: %s\n", s1.getString().c_str());
printf("S2: %s\n", s2.getString().c_str());
}
Here's the output:
UNIX> berttest3 Creating new Bert (Dog) Creating new Bert (Rat) S1: Dogbert S1: Dogbertbert S2: Dogbert Bert destructor called (Dogbert) Bert destructor called (Dogbertbert) UNIX>All straightforward. As you see, when you say s2 = s1, it copies everything -- in particular, s2.str is copied from s2.str. Then when you add a "bert" to s1, s2 is unmodified.
Of course, this code is a bit weird -- why should we initialize s2 to be "Rat", when we're simply going to copy it from s1?. This is an example where declaring a variable in the middle of the code makes sense. Look at berttest3a.cpp:
#include < stdio.h >
#include "bert.h"
main()
{
Bert s1("Dog");
s1.add_bert();
printf("S1: %s\n", s1.getString().c_str());
Bert s2 = s1;
s1.add_bert();
printf("S1: %s\n", s1.getString().c_str());
printf("S2: %s\n", s2.getString().c_str());
}
Here we set s2 right when we declare it, and for that reason,
we don't need to actually call the constructor. Note in the output,
that the constructor method is only called for s1 and not for
s2. However, at the end of the main() routine, destructors
are called for both s1 and s2.
UNIX> berttest3a Creating new Bert (Dog) S1: Dogbert S1: Dogbertbert S2: Dogbert Bert destructor called (Dogbert) Bert destructor called (Dogbertbert) UNIX>Now, what about passing a Bert as a parameter? Here's berttest4.cpp and its output:
#include < stdio.h >
#include "bert.h"
void a(Bert s2)
{
s2.add_bert();
printf("S2: %s\n", s2.getString().c_str());
}
main()
{
Bert s1("Dog");
a(s1);
printf("S1: %s\n", s1.getString().c_str());
a(s1);
}
UNIX> berttest4
Creating new Bert (Dog)
S2: Dogbert
Bert destructor called (Dogbert)
S1: Dog
S2: Dogbert
Bert destructor called (Dogbert)
Bert destructor called (Dog)
UNIX>
Did you expect this? You'll note that the parameter s2 is a
distinct Bert from s1, but the constructor method
was never called. Why is this? Because evidently, when you declare an
instance of a class
to be a copy of an existing instance (as with s2 in berttest3a
above) you don't need to call the constructor -- it simply
copies every member of the class. Arguments to procedures fit this definition --
they are copies of their calling arguments. When a() returns however,
the destructor for s2 is called.
Note that s2 is definitely a copy of s1. Why? Because
if it were not a copy, then s1 would be "Dogbert" when
it is printed by the main() routine.
How about global variables? Look at berttest5.cpp and its output:
#include < stdio.h >
#include "bert.h"
Bert Global_bert("Rat");
main()
{
Bert s1("Dog");
s1.add_bert();
printf("S1: %s\n", s1.getString().c_str());
printf("GJ: %s\n", Global_bert.getString().c_str());
}
UNIX> berttest5
Creating new Bert (Rat)
Creating new Bert (Dog)
S1: Dogbert
GJ: Rat
Bert destructor called (Dogbert)
Bert destructor called (Rat)
UNIX>
No problem -- the constructor for the global variable is called before
the main() routine gets invoked, and the destructor is called
after main() returns. How do we know that? Well, try
berttest6.cpp:
#include < stdio.h >
#include "bert.h"
Bert Global_bert("Rat");
main()
{
Bert s1("Dog");
s1.add_bert();
printf("S1: %s\n", s1.getString().c_str());
printf("GJ: %s\n", Global_bert.getString().c_str());
exit(0);
}
UNIX> berttest6
Creating new Bert (Rat)
Creating new Bert (Dog)
S1: Dogbert
GJ: Rat
Bert destructor called (Rat)
UNIX>
As you see, only one destructor gets called. Actually, that surprises
me -- I thought none would get called. Think about it. Exit()
never returns. Therefore the destructor for s1 should never get
called, and as you see, it doesn't.
The destructor that gets called is for
Global_bert. This is because exit() calls a bunch of cleanup
code (for example, it flushes and closes all open files). It therefore
also calls destructors for all global objects, in case they need to
do some final cleanup actions. We can test this, of course. Any
ideas how? One way is in
berttest7.cpp, where _exit()
is called instead of exit() -- _exit() exits immediately
without doing any clean-up code. And as you see, no destructors are
called in berttest7:
UNIX> berttest7 Creating new Bert (Rat) Creating new Bert (Dog) S1: Dogbert GJ: Rat UNIX>A few more examples. We can return an instance of a class from a procedure. Let's see what happens. Look at berttest8.cpp and its output:
#include < stdio.h >
#include "bert.h"
Bert a()
{
Bert s2("Rat");
s2.add_bert();
printf("S2: %s\n", s2.getString().c_str());
return s2;
}
main()
{
Bert s1("Dog");
printf("S1: %s\n", s1.getString().c_str());
s1 = a();
printf("S1: %s\n", s1.getString().c_str());
}
UNIX> berttest8
Creating new Bert (Dog)
S1: Dog
Creating new Bert (Rat)
S2: Ratbert
Bert destructor called (Ratbert)
S1: Ratbert
Bert destructor called (Ratbert)
UNIX>
Now, try berttest9.cpp:
#include < stdio.h >
#include "bert.h"
Bert a()
{
Bert s2("Rat");
s2.add_bert();
printf("S2: %s\n", s2.getString().c_str());
return s2;
}
main()
{
Bert s1("Dog");
s1.add_bert();
printf("S1: %s\n", s1.getString().c_str());
printf("A(): %s\n", a().getString().c_str());
printf("S1: %s\n", s1.getString().c_str());
}
You should be able to figure
this one out before you look at the output:
UNIX> berttest9 Creating new Bert (Dog) S1: Dogbert Creating new Bert (Rat) S2: Ratbert Bert destructor called (Ratbert) A(): Ratbert Bert destructor called (Ratbert) S1: Dogbert Bert destructor called (Dogbert) UNIX>Finally, remember that this automatic calling of constructors and destructors only applied for non-pointers. If you use pointers, you have to use new and call delete when you are done with the class. For example, look at berttestp1.cpp:
#include < stdio.h >
#include "bert.h"
main()
{
Bert *s1;
s1 = new Bert("Dog");
s1->add_bert();
printf("S1: %s\n", s1->getString().c_str());
s1 = new Bert("Rat");
s1->add_bert();
printf("S1: %s\n", s1->getString().c_str());
}
This code compiles and runs just fine, but you'll notice that no destructors
were called:
UNIX> berttestp1 Creating new Bert (Dog) S1: Dogbert Creating new Bert (Rat) S1: Ratbert UNIX>This is because you have to call delete if you want to explicitly destroy the class. Often, it's not a problem if you're simply going to allocate memory and exit the program, but if you overwrite a pointer, you should delete its contents if they are not going to be used again. For example, in the above code, you should call delete s1 before the call s1 = new Bert("Rat").
Finally, when an instance of a class is destroyed, the destructors for all member class instances are recursively destroyed. For example, look at rectest.cpp:
#include < stdio.h >
#include <string.h>
using namespace std;
class TempClass1
{
public:
TempClass1();
~TempClass1();
protected:
string s;
};
class TempClass2 {
protected:
TempClass1 t;
};
TempClass1::TempClass1()
{
s = "Mickey";
printf("Making TC1: %s\n", s.c_str());
}
TempClass1::~TempClass1()
{
printf("Destroying TC1: %s\n", s.c_str());
}
main()
{
TempClass2 t;
}
As you can see, the program does nothing except construct and destroy a
TempClass2. This constructs and then destroys a TempClass1,
which means that the program has the following output:
UNIX> rectest Making TC1: Mickey Destroying TC1: Mickey UNIX>