Tải bản đầy đủ (.pdf) (47 trang)

Tài liệu Giáo trình C++ P3 pdf

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (480.87 KB, 47 trang )


www.gameinstitute.com Introduction to C and C++ : Week 3: Page 1 of 47










Game Institute



































by Stan Trujillo



Week
3

Introduction to C and C++

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 2 of 47






© 2001, eInstitute, Inc.

You may print one copy of this document for your own personal use.
You agree to destroy any worn copy prior to printing another. You may
not distribute this document in paper, fax, magnetic, electronic or other
telecommunications format to anyone else.















This is the companion text to the www.gameinstitute.com
course of the
same title. With minor modifications made for print formatting, it is
identical to the viewable text, but without the audio.












www.gameinstitute.com Introduction to C and C++ : Week 3: Page 3 of 47



Table of Contents

Introduction to C++ 4
Lesson 3 – Classes 4
Member Functions 6
Encapsulation 12
Construtors 14
Destructors 16
The this Pointer 20
The Assignment Operator 20
Overloaded Operators 24
Copy Constructors 24
The CmdLine Sample 25
Inheritance 32
Inheritance Syntax 34
Derived Functionality 35
Virtual Functions 37
The VirtualPlayer Sample 41
What’s next? 47


www.gameinstitute.com Introduction to C and C++ : Week 3: Page 4 of 47

Introduction to C++
A GameInstitute course by Stan Trujillo

In Lesson 1 we discussed functions. In Lesson 2 we discussed data structures. In this lesson, we introduce
classes, which blend functions and data structures to create a powerful language feature: objects. Using
objects, entities can be created that are intelligent and robust in a way that functions or data structures
alone cannot be. This single feature is what sets C++ apart from C. At the same time, the C ancestry is not
discarded. C++ still benefits from the speed advantages of C, offering much better performance than other
object-oriented languages.

Using only functions and data structures, as covered in the previous two lessons, is known as procedural
programming. Object-oriented programming uses a new paradigm, and as such provides a different way
to approach programming.

Some advocates of object-orient programming often disparage procedural programming, and insist that
programmers give themselves over to this new paradigm completely. They are critical of programmers
who mix paradigms (or, worse yet, use C++, but do not use objects), and they are supporters of “pure”
object-oriented languages such as Java. The implication is that object-oriented programming is so
radically different from procedural programming that everything that you know about procedural
techniques should be discarded, and that every feature in every program should be represented as an
object or set of objects.

But object-oriented programming does not and cannot entirely replace procedural programming. The
object-oriented purists seem to forget that objects use functions. Procedural issues such as function calls,
arguments, return values, automatic variables, and data types apply to object-oriented programming as
much as they do to procedural programming. Object-oriented techniques don’t replace procedural
techniques; they augment and enhance them.


We didn’t spend the first two lessons studying procedural programming features just for historical
reasons. Nor did we do it to demonstrate how horrible these concepts are in contrast to object-oriented
features. We need the foundation that procedural programming provides in order to be effective object-
oriented programmers. We won’t discard what we’ve learned thus far; we’ll use it to create objects.

Having said that, good object oriented programming does require a different mindset. For programmers
with extensive procedural experience in particular, this can be a rough transition, because they have to
break learned habits. It is not uncommon for these programmers to be very slow to fully adopt object-
oriented approaches, which results in code that uses a blend of techniques. In this sense, new
programmers who start with object-oriented languages have an advantage. They learn to use objects from
the start, and don’t have to fight established thought patterns.
Lesson 3 – Classes
C++ uses the term class to describe an object definition. A class is a data type, just like an intrinsic data
type or a structure. As a data type, classes are blueprints that describe data entities, and can be used to
declare variables. Instead of being called variables, however, instances of a class are called objects.
Before we can create an object, therefore, we must define the class that describes it. One way to create a
class is to use the class keyword, like this:

class Player
{

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 5 of 47
char name[80];
int score;
};

This definition creates a class called Player. The Player class defines two members, name, and score,
which can be used to store data about the player. In object-oriented lingo, these data definitions are called
data members. In this case the Player class is said to have two data members.


The Player class shown above looks almost exactly like the Player structure from Lesson 2. The only
difference is the class keyword (the Player structure uses the struct keyword). In fact, there is virtually
no difference between a class and a structure. Consider these two definitions:

class Player
{
char name[80];
int score;
};

struct Player
{
char name[80];
int score;
};

Both of these definitions define a data type called Player. Both of these types can be used to create
variables, and both contain the same data members. In C++, a class and a structure are virtually identical.
The only difference is the default permissions. By default, the struct keyword results in a data type whose
members are public, whereas the members of a class are private.

We know that the contents of the struct are freely accessible (public) because we assigned and inspected
them freely in Lesson 2. A class, however, because it is private by default, does not allow access to its
data members:

class Player
{
char name[80];
int score;

};

Player player;
player.score = 0; // compiler error! ‘score’ is private

What is the point of a data structure with elements that can’t be accessed? We’ll get to that soon. First,
let’s talk about how we can control the accessibility of data members.

Access to data members is controlled with the C++ public and private keywords. Using the public
keyword makes everything that follows it freely accessible. Likewise, the private keyword restricts
access items that follow it. Using the public keyword, we can rewrite the class-based version of Player
like this:

class Player
{
public:

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 6 of 47
char name[80];
int score;
};

This version of Player provides full access to its data members because they are explicitly declared to be
public. The default private permission level of the class keyword is overridden by the use of the public
keyword. This version of Player behaves exactly like the Player structure used in Lesson 2.

The default permission level of struct can be overridden with the private keyword:

struct Player
{

private:
char name[80];
int score;
};

This version is effectively the same as the original class-based version; its data members are not publicly
accessible.

The permission level of each data member can be controlled separately:

struct Player
{
char name[80];
private:
int score;
};

In this case, name is publicly accessible (because struct is used), but score is not. The effect of public
and private takes effect for all of the members that follow, until either the end of the definition is reached
(the closing curly brace), or another permissions modifier is encountered. There is no limit to the number
of permission modifiers that can be used, and there is no rule against redundant modifiers (usages of
public or private that do not change the current permission setting, because the specified permission is
already in effect.) When used to modify the access permissions for members, both keywords must be
followed by a colon.

Despite the fact that there is very little difference between class and struct, the term structure is used to
refer to data types that contain only public data members, and the term class is used to refer to types that
contain a combination of data types and member functions. We’ll talk about member functions next.
Member Functions
The goal of object-oriented programming is to create objects that are safe, robust, and easy to use.

Although public data members are easy to use, they provide a level of access that is discouraged because
they allow anyone to assign any value to any data member. As a result, data members should almost
always be private. Instead of direct access, data members are usually accessed via member functions, like
this:

class Player
{
public:
void SetScore(int s);

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 7 of 47
int GetScore();
private:
char name[80];
int score;
};

By adding member functions, we now have access—albeit indirect access—to data members that are
private. The SetScore function provides a way to assign score, and GetScore provides support for score
retrieval (for now we’re providing member functions for only the score data member.)

A member function is just like a regular function except that it belongs to a class. It is declared within the
class definition, and, as a member of that class, is given access to all data members and member functions
present in the class, private or otherwise. Member functions can’t be used outside of the context of the
class. The member functions that are now part of the Player class can’t be called without a Player object
(a variable of the Player type.) This in is contrast to the functions we’ve been using thus far, that are not
part of a class. Non-class member functions are called C functions, or global functions.

In the example above, the body of the GetScore and SetScore member functions are not provided, so this
class is incomplete. The class has been defined, and the two member functions have been declared, but

they have not been defined. We can complete the class by providing the member function bodies inside
the class, like this:

class Player
{
public:
void SetScore(int s) { score = s; }
int GetScore() { return score; }
private:
char name[80];
int score;
};

Notice that the semicolon following each member function in the previous example has been replaced
with the function body for each member function. The SetScore function body uses the s parameter to
assign the private data member score, and the GetScore function simply returns the value of score.

As we discussed in Lesson 1, it is typical to begin function bodies with an opening curly bracket on the
line following the function name, but simple member functions that are defined within a class definition
are generally an exception to this rule. The formatting shown above is typical for simple accessor
functions—member functions that merely provided access to a data member.

Alternatively, member functions can be defined outside the class definition, like this:

class Player
{
public:
void SetScore(int s);
int GetScore();
private:

char name[80];
int score;
};


www.gameinstitute.com Introduction to C and C++ : Week 3: Page 8 of 47
void Player::SetScore(int s)
{
score = s;
}

int Player::GetScore()
{
return s;
}

Although this syntax is not normally used for simple accessor functions like SetScore and GetScore, it is
common for functions with more complex function bodies, and for member functions whose class is
defined in a header file (we’ll talk about that in Lesson 4).

Member functions that are defined outside the class definition must use the C++ scope resolution
operator, which is two colons (::). The name that appears to the left of this operator is the class to which
the member function belongs, and the name to the right is the member function name. Defining member
function outside the class definition is exactly like defining a global function, except that the scope
resolution operator is used to indicate the class or scope to which the function belongs. If no scope
resolution operator were used in the definitions above, the compiler would have no way to know that
GetScore and SetScore belong to the Player class. This would cause a compiler error because both
functions refer to score, which doesn’t exist outside the Player class.

Providing member functions is safer than allowing direct access to data members because you—as the

author of a class—can dictate what data values are allowed for each data member. For example, suppose
that the game you are writing prohibits the use of negative scores. If the score data member were public,
it could be assigned negative values from anywhere in the game code. But because we’ve made it private,
we’re requiring that it be set via the SetScore member function. This gives us the opportunity to enforce
restrictions on values passed to SetScore. One strategy would be to implement SetScore like this:

void Player::SetScore(int s)
{
if (s >= 0)
{
score = s;
}
}

This prevents score from being set to a negative value. If a negative value is provided to SetScore, it is
simply ignored. This is less than ideal, however, because if a negative value is passed to SetScore, no
indication is given that the function call had no effect. We could change the return type of SetScore to
bool, and return true if a valid score is provided, and false otherwise, but this means that the caller is
expected to check the return value of SetScore each time it is called. Return values are easy to ignore—
there is no way to insure that they are checked, or even retrieved.

A better solution is to use an assert. An assert is a macro that can be used to force specific conditions
within your code. If the condition passed to assert is non-zero (true), assert has no effect. But if the
condition resolves to 0 (false), assert halts the executable, and displays the name of the source file and the
line where the assertion failed. Using assert requires that the assert.h header file be included, and looks
like this when used with the SetScore function:

void Player::SetScore(int s)

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 9 of 47

{
assert( s >= 0 );

score = s;
}

This code dictates that values sent to SetScore must be non-negative. If not, execution will halt, and
you’ll be informed the exact location of the offending code.

The neat thing about assert is that, as a macro, it is defined in such a way that it has absolutely no effect
in release builds. As a result, assert can be used liberally, and they’ll announce problems with the code in
Debug builds, but the release build will run optimally.

Before we continue, let’s provide member functions for the name data member in the Player class. The
first step is to declare these functions within the Player class, granting them public access:

class Player
{
public:
void SetScore(int s);
int GetScore();
void SetName(char* n);
char* GetName();
private:
char name[80];
int score;
};

The SetName member function takes a char pointer as an argument. This will allow callers to provide the
address of the player name. Likewise the GetName function returns a pointer to the name, and not the

data itself. SetName looks like this:

void Player::SetName(char* n)
{
assert( n );

strcpy( name, n );
}

This implementation of SetName uses the provided pointer to copy the contents of the name data
member. Notice that an assert is used to mandate that n is non-zero. When dealing with pointers, it’s
important to account for the possibility that null pointers will be provided. In this case providing a null-
pointer is not allowed, as the SetName function needs the address of a valid string from which to copy the
player name.

Now we’re ready to write the GetName function. This function returns a pointer to a char, so we can
write it like this:

char* Player::GetName()
{
return name;
}


www.gameinstitute.com Introduction to C and C++ : Week 3: Page 10 of 47
GetName simply returns the address of the name array. No address of operator is required, because name
is an array, and therefore a pointer. This allows us to provide the player name to the calling function
without making a copy of the data. The GetName function can be used like this:

Player localPlayer; // global object, initialized with a player name elsewhere


void DisplayLocalPlayerName()
{
char* pName = localPlayer.GetName();

// (code to display the name on the screen)
}

This code works fine, but only if the calling code uses the returned pointer for read-only purposes. There
is nothing preventing the code from assigning the content of our private data member, so an errant
programmer on the team might do this:

Player localPlayer; // global object, initialized with a player name elsewhere

void DisplayLocalPlayerName()
{
char* pName = localPlayer.GetName();

strcpy( pName, “surprise!” );

// (code to display the name on the screen)
}

This code, using the pointer that we’ve provided, writes a new string to a private data member. The rogue
programmer has overwritten the player name with the string “surprise!” This constitutes a breech in the
security of our class because our intention was that the only way to assign the name data member was
with the SetName member function. Luckily, this problem can be avoided with the const keyword.

C++ provides the const keyword to allow the declaration of variables that are “read-only.” Const
variables are just like regular variables, but they cannot be modified. The only way to assign a value to a

const variable is to initialize it during declaration. Here’s an example:

const int MaxPlayers = 10;

cout << “MaxPlayers = “ << MaxPlayers << endl; // this works fine

MaxPlayers = 9; // this will not compile

This example declares an integer called MaxPlayers, and initializes it to 10. This variable can be used at
will, as long as the operations involved don’t attempt to modify its value. Any attempt to do so will fail at
compile-time. The compiler simply refuses to compile code that attempts to misuse a const variable.
(Notice that in the example above, we’ve broken our rule about variable names, which usually begin with
a lower-case letter. Const variables are exempt from this rule, and usually begin with an upper-case letter,
and sometimes use all uppercase names.)

Without resorting to crude methods that we won’t discuss here, there is no way to overwrite the value of a
const variable. The only way to assign a value is to do so during initialization. In fact, C++ won’t let you
declare a const variable without initializing it:

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 11 of 47

// compiler error! const variables must be intialized when declared
const int MaxPlayers;

Although it is under-used by most programmers, const is a great way to increase the security of your
code, and to indicate to programmers using your classes and functions how you intend for them to be
used. We can rewrite the Player::GetName function using const like this:

class Player
{

public:
void SetScore(int s);
int GetScore();
void SetName(char* n);
const char* GetName();
private:
char name[80];
int score;
};

const char* Player::GetName()
{
return name;
}

Now the contents of the name data member are safe from unauthorized tampering, thus thwarting our
troublesome teammate. Given our changes, let’s revisit his code.

Player localPlayer;

void DisplayLocalPlayerName()
{
char* pName = localPlayer.GetName(); // compiler error #1

strcpy( pName, “surprise!” ); // compiler error #2

// (code to display the name on the screen)
}

This code won’t compile for two reasons. First, you cannot assign a non-const pointer using a const

pointer. C++ doesn’t allow this because it would allow you to “throw away” the const modifier, meaning
that the code above would still compile and breech object security. In order to call the GetName function,
the author of this code will have to modify it to use a const pointer:

const char* pName = localPlayer.GetName();

This modification allows the pointer assignment to compile, but the offending code will still fail to
compile:

strcpy( pName, “surprise!” ); // ha! nice try

With the addition of the const keyword, we’ve prevented our private data from being overwritten, and
we’ve done so without sacrificing the savings of returning the address of the player name instead of a

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 12 of 47
copy. The caller still gets a pointer to our private data member, but because it is const, is unable to use it
for anything other than read-only operations.
Encapsulation
You may be wondering why we’re suddenly so concerned with accessibility and security. Why would you
have someone on our team who is trying to sabotage your objects? C++ provides the ability to limit
access to specific data members and member functions for several reasons, security being just one of
them. Another reason is encapsulation.

Encapsulation is the practice of hiding how an object works. The idea is that member functions and data
members that are specific to the internal workings of a class are made private, and therefore inaccessible
to users of the class. A public set of member functions are provided as well, which allow the object to be
used, but without exposing any details of the inner workings. This public portion of the class is called an
interface.

Encapsulation allows a class to be used via a clear and concise interface, without exposing the gory

details of how the class works. This accomplishes two purposes: it allows complex functionality to be
provided in a simple package, and it makes it possible for the author of the class to modify its inner
workings without changing the way the class is used.

The Player class, for example, provides an interface composed of four public functions:

• SetScore
• GetScore
• SetName
• GetName

Users of our class only need to know about these four functions. They don’t need to know about the data
members, because they are specific to how the class is implemented. This is precisely why we declared
the member functions at the top of the class, and the data members at the bottom. It makes no difference
to the compiler, but to a programmer that is looking at the Player class definition with the intention of
figuring out how to use it, the interface is prominently displayed at the top, while the data members are
tucked away at the bottom (this practice has more of a visual impact with larger classes):

class Player
{
// the public interface is best provided at the top of the class definition
public:
void SetScore(int s);
int GetScore();
void SetName(char* n);
const char* GetName();

// the rest of the class is private, and of no concern to users
private:
char name[80];

int score;
};

To demonstrate the power of encapsulation, let’s assume that we have used the Player class in a large
game. The game code uses the class to create an array of player objects which are then passed reference to

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 13 of 47
various modules, handling tasks such as game initialization, network services, custom player settings, and
a high score screen. The Player class is used in dozens of lengthy and complex source code files.

Given its extensive use, it would be a pain to modify the class. Any change to one of the existing interface
functions, for example, would “break” the code, causing compilation to fail. Each usage would then have
to be found and modified to reflect the interface changes. This could involve hundreds of changes.

But to change the implementation of the class—how it works—is easy. Notice that we are currently using
a fixed array size of 80 characters. This is a waste of memory (although not a very big one) because most
player names are likely to be less than 15 characters. We could simply change the size of the array to 15,
but that’s less than ideal. What if a player wants to use a name that’s longer than 15 characters? We don’t
want to impose an unreasonable limit just to save few dozen bytes.

Instead, we can modify the implementation of the Player class to use dynamic memory, allowing each
object to allocate just enough memory for the provided player name. Instead of a fixed array, we can use a
pointer, like this:

class Player
{
public:
void SetScore(int s);
int GetScore();
void SetName(char* n);

const char* GetName();
private:
char* pName;
int score;
};

To support this new type, we’ll need to modify the SetName member function to allocate memory
dynamically:

void Player::SetName(char* n)
{
assert( n );

int len = strlen( n );
pName = new char[len+1];
strcpy( pName, n );
}

This version of SetName uses the strlen function (another standard function, which, like strcpy, is
declared in string.h) to retrieve the length of the string provided as n. The new operator is then used to
allocate an array that is equal to the name length, plus one extra character for the null terminator. The
strcpy function is used to copy the contents of the provided string into the newly allocated array.

Now the Player class makes better use of memory, and provides no restriction on player name length at
all. And yet the class interface is unchanged. The entire game compiles without further modification, and
automatically makes use of the new implementation. The vast majority of the game code hasn’t changed,
but the way that it works has.

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 14 of 47
Construtors

A constructor is a special function that gets called automatically whenever an object is created.
Constructors are typically used to assign initial values to data members so that the new object is in a safe
state before it is used. C++ employs a special syntax for constructors: they have the same name as the
class. We can add a constructor to the Player class this way:

class Player
{
public:
Player();
void SetScore(int s);
int GetScore();
private:
char* pName;
int score;
};

This constructor is named Player, just like the class name. Using another name would be legal, but would
not result in a constructor. Only constructors are called automatically when instances of a class are
created.

Unlike global functions and member functions, constructors cannot return values, so no return type
precedes the Player constructor declaration above. This limitation means that there is no way to report
failures using a return type. As a result, it is generally a bad idea to perform operations that are reasonably
likely to fail in a constructor. Attempting to open a file, for example, is best not performed in a
constructor because the file may have been moved, deleted, or corrupted prior to execution.

For the Player class we can use the constructor to initialize the class data members. Like member
functions, constructor bodies can be defined either inside or outside the class definition. Because this
constructor performs more than one operation, we’ll opt for the external syntax:


Player::Player()
{
pName = 0;
score = 0;
}

The external syntax requires the use of the scope resolution operator. For constructors this has the odd
result of two identical names that appear next to each other, separated by two colons.

The Player constructor, as defined above, simply assigns the two data members two zero. Notice that if
we were still using the array-based version of Player, this code wouldn’t compile. Despite the fact that
arrays are represented as pointers, they cannot be assigned to zero the way that pointers can. The
constructor shown above is based on the dynamic memory modification made to the Player class in the
previous section.

Constructors are special because they are called automatically. In fact, for a class like Player that has a
constructor that takes no arguments, there is no way to create an instance of Player without automatically
invoking the constructor. This is true regardless of whether the variables are automatic or dynamic:

Player autoPlayer; // constructor is called automatically
Player* dynPlayer = new Player; // constructor is called automatically

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 15 of 47

In either case, the resulting variable will have data members that are initialized to zero. If an array of
players is created, the constructor will be called once for each element:

Player playerArray[100]; // constructor is called 100 times, once for each element

This is a very nice feature, as it eliminates the possibility of using an un-initialized variable, and frees

uses of the class from the vagaries of variable and structure initialization. A constructor that takes no
arguments is called a default constructor.

Constructors can be written to take arguments. The most common use for constructor arguments is to
provide initial values for some or all of the data members. This allows us to provide a constructor for the
Player class that takes a player name and initial score:

class Player
{
public:
Player();
Player(char* n, int s);
void SetScore(int s);
int GetScore();
private:
char* pName;
int score;
};

Player::Player(char* n, int s)
{
pName = 0;
SetName( n );

assert( s >= 0 );
score = s;
}

A class can have any number of constructors, as long as each one has a different argument list. This is
possible because of function overloading, as discussed in Lesson 1. In this case we’ve added a constructor

that takes a char pointer and an integer for the purposes of providing an initial name and score for the new
Player object. The constructor body initializes the data members. The score data member is initialized
directly, and the pName data member indirectly—through the use of the SetName member function.

Class member functions are free to call other class member functions, and can do so whether they are
public or private. In this case we opted to call SetName because the alternative would be to allocate the
dynamic memory required to store the player name, and that code has already been written. By calling
SetName, we’re re-using that code instead of wasting time writing code that is virtually identical.

The new constructor can be used like this:

Player player1( “slowpoke”, 100 );

This syntax looks like a function call—which is appropriate, because it is. This code creates a new Player
object, and initializes it with the new constructor. The result is an object called player1 with custom
values assigned to the internal data members. Notice that we didn’t replace the original constructor. Like

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 16 of 47
regular functions, C++ allows constructors to be overridden. The compiler uses the correct constructor
depending on how the object is created:

Player defaultPlayer; // calls the default constructor
Player customPlayer( “slowpoke”, 100 ); // calls the new constructor

In addition to safety, constructors provide a means of dictating how a class is used. For example, we
could force anyone using the Player class to provide a player name and score by omitting the default
constructor. This would leave the class with only one constructor—one that requires a player name and
score.

As long as a class provides at least one constructor, objects of that type cannot be created without

invoking a constructor. If the only constructor present requires arguments, then providing those
arguments is the only way to create an instance of the class. If no constructors are present, new instances
of the class will be un-initialized, just as is the case with structures.
Destructors
The compliment to the constructor is the destructor. A destructor is a special member function that is
automatically called when the object is being destroyed.

Destructors aren’t always required. Classes that contain only non-pointer data members, for example,
don’t typically need destructors because the memory used to represent the object itself is released
automatically. But for classes that use external resources such dynamic memory, destructors are often
used to release those resources.

Our new version of Player, for example, because it allocates dynamic memory to store the player name,
needs a destructor so that this memory can be released when the object is destroyed. In its current state the
Player class allocates the required memory when the SetName function is called, but never releases it.
This is called a memory leak.

Memory leaks are specific to memory that is allocated dynamically. It is not possible to leak the memory
used by local variables. Memory leaks can’t be detected by the compiler and they don’t generate runtime
errors, so they are hard to detect. As a result, memory leaks are very common. It is not unusual for
commercial applications, including games, to be shipped with memory leaks.

This isn’t a big deal if the offending code allocates just a few hundred bytes or less, but if it allocates
large memory buffers, or if the leak is in a function that gets called frequently, the cumulative effect will
cause the application to perform poorly, or even crash. Often, memory leaks are found in classes that
either don’t have a destructor, or have a destructor that doesn’t release all of the memory that the class
uses.

We can fix the memory leak in the Player class with a destructor. First, we need to declare it in the class
definition:


class Player
{
public:
Player();
Player(char* n, int s);
~Player();
void SetScore(int s);

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 17 of 47
int GetScore();
private:
char* pName;
int score;
};

C++ uses the tilde (~) together with the name of the class to indicate a destructor. As a result, destructors
look very much like constructors. Like constructors, destructors cannot return values. Unlike constructors,
destructors cannot take arguments, so they can’t be overloaded. At most, a class can have one destructor.

The Player class definition above declares a destructor, but doesn’t define it. We can define the destructor
body internally or externally, but we’ll opt for external:

Player::~Player()
{
delete [] pName;
}

An external destructor like this one looks very much like the definition for a default constructor (a
constructor that doesn’t take any arguments.) The only difference is the tilde. Using the delete operator,

this destructor simply releases any dynamic memory that the class is using for player name storage.

Notice that the pName data member might be zero. Creating a Player object with the default constructor,
for example, and they destroying the object before a player name has been assigned would mean that the
pName data member would be initialized to zero (by the default constructor), and then used in that state
by the destructor. This is legal. C++ implements the delete operator in such a way that passing a pointer
that points to zero will have no effect.

Now, regardless of how objects of the Player type are created, any memory allocated for the player name
will be released when the object is destroyed. This code demonstrates the case where a Player object is
created as an automatic variable:

void TestFunc()
{
Player p;

p.SetName( “slowpoke” ); // this call allocates dynamic memory

// the object ‘p’ is destroyed automatically when this function returns
// the Player destructor will be called, releasing the dynamic memory
}

Likewise, player objects created dynamically don’t leak memory anymore either, assuming that we
remember to destroy the player object itself with delete. In the code below, the lifespan of the player
object is controlled by two functions, one that is called during game startup, and the other during game
shutdown. The player object exists for the duration of the game:

Player* localPlayer = 0; // global pointer

void DoStartup()

{
localPlayer = new Player;
localPlayer->SetName( “unnamed player” );

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 18 of 47

// initialize other game objects
}

void DoShutdown()
{
delete localPlayer;

// delete other game objects
}

This example makes use of a global Player pointer, allowing more than one function to access the object.
The DoStartup function assigns this pointer using the new operator, creating a new instance of the
Player class. The SetName member function is then called with a default player name. Internally to the
Player object, this results in a dynamic memory allocation, but that’s of no concern to users of the Player
class.

The DoShutdown function is designed to perform the release of objects used by the game. This function
uses the delete operator to destroy the player object created earlier. This command results in a call to the
class destructor, which uses delete again, this time to release the memory used to store the player name.

Unfortunately, the addition of a destructor to the Player class has fixed the memory leak, but only for the
simple case where the player name is set once. This class will still leak memory if the SetName function
is called twice. To fix this leak we need to modify the SetName member function. As a reminder, this is
how it looks currently:


void Player::SetName(char* n)
{
assert( n );

int len = strlen( n );
pName = new char[len+1];
strcpy( pName, n );
}

The problem with this version is that it assumes that pName hasn’t already been assigned with a previous
call. The address stored in pName is simply overwritten as new memory is allocated with the call to new.
This problem can be fixed by first using the delete operator to release any existing memory:

void Player::SetName(char* n)
{
assert( n );

delete [] pName;

int len = strlen( n );
pName = new char[len+1];
strcpy( pName, n );
}

This modification works whether or not pName was assigned to point to a player name previously. If not,
then pName will be zero, which delete will simply ignore. If another name has been assigned, this

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 19 of 47
modification will release the memory used previously before allocating a new array. Now the Player

class can handle any of the three cases:

• No name is assigned at all
• One name is assigned
• Multiple names are assigned

To demonstrate this final case, let’s revisit the startup/shutdown example, and add a new function:

Player* localPlayer = 0; // global pointer

void DoStartup()
{
localPlayer = new Player;
localPlayer->SetName( “unnamed player” );

// initialize other game objects
}

void GetActualPlayerName()
{
char realName[80];

// code to get name from user

assert( localPlayer );

// This call deletes the memory used to store “unnamed player”,
// and allocates a new character array for the name stored
// in the realName array.
localPlayer->SetName( realName );

}

void DoShutdown()
{
delete localPlayer;

// delete other game objects
}

Now we’ve added a third function, which may or may not get called, that overrides the default player
name assigned in the startup code. This second call to SetName deletes the memory used for the default
name, and allocates a new array for the storage of the player’s name.

The code above does not constitute a complete program. For one thing, there is no main function. This
code demonstrates how an object might be used in a game when the object is required for longer than the
execution of a single function. Although this isn’t a complete program, it does hint at the fundamental
layout for game design because games require (often lengthy) startup procedures, and these functions are
typically matched to a function that performs shutdown procedures. We’ll talk about this in detail in the
next lesson.

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 20 of 47
The this Pointer
In order to continue learning about objects, it’s necessary to introduce the fact that C++ provides a
keyword that is valid within the scope of every structure and class, which indicates the address of the
current object. The this keyword is a pointer to the current object. The type of this varies; it is always the
type of the class within which this is being used.

We’ll talk about some cases where using this is necessary soon, but for now it is enough to know that this
is present, and that it can be used as a means of accessing object members. For example, anytime a data
member or member function is used, it can be prefixed with this, like so:


void Player::SetScore(int s)
{
score = s;

this->score = s;
}

Both of the statements in this function accomplish the same effect: they both assign the score data
member. In this case there’s no reason to use this, but in the next section we’ll present a case where the
use of this is required.
The Assignment Operator
C++ allows the assignment operator (=) to be used to assign the data from one variable to another. This is
true for intrinsic data types, structures, and classes (but not arrays.) For example:

// making a copy of an intrinsic type:
int a, b = 44;
a = b; // ‘a’ is now 44

struct Vector
{
float x, y;
};

// making a copy of a structure
Vector v1, v2 = { 1.0f, 1.0f };
v1 = v2; // ‘v1’ is now equal to ‘v2’ ( v1.x == 1.0f and v1.y == 1.0f )

// making a copy of a class
Player p1, p2( “slowpoke” );

p1 = p2; // ‘p1’ is now equal to ‘p2’ (it has “slowpoke” for a player name)

This is possible because, as long as the variables on either side of the assignment operator are of the same
type, C++ generates code that copies all of the data in the right-hand variable to the left-hand variable.
This works fine for intrinsic types, but is a potential source of trouble with structures and classes—
particularly if the structure or class contains pointers.

For the version of the Player class that we defined earlier that used a fixed array of 80 characters to store
the player name, the assignment operator can be used to assign variables with no unwanted side effects.
But for the later version that uses a char pointer, the result of using the assignment operator is that the
two objects will use the same instance of dynamic memory. The assignment operator copies data from

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 21 of 47
one variable to the other, regardless of the nature of the data, so in this case a duplicate copy of the player
name pointer (but not the player name itself) is made. This situation is illustrated here:



Consider what happens if, subsequent to using the assignment operator, the SetName member function is
used on the p1 object. The original memory buffer—which is shared by the two objects—is deleted, a
new buffer is allocated, and the pName member of p1 object is updated to point to it. The p2 object now
points to a memory location that is no longer allocated:



We could modify the code to include a member function that modifies the data in the existing string
instead of allocating a new string, but this too would be problematic because, as a shared buffer, any
changes to the player name would be reflected in both objects. Also, this would prohibit the use of a
player name that is longer than the original, because the new name would have to fit into the existing
array.


The effect we want is for the assignment operator to be smarter about the data contained in the Player
class. Instead of simply making a copy of the data in the object, it should allocate a new buffer for
exclusive use by the new object. This arrangement would look like this:


www.gameinstitute.com Introduction to C and C++ : Week 3: Page 22 of 47


Now either object can be modified at will because each has it’s own memory for the storage of the player
name. This is called a deep copy, and is necessary whenever an object uses dynamic memory that
contains data that is specific to that object.

C++ allows the meaning of operators to be overloaded. This is called operator overloading, and, among
other things, can be used to define member functions that provide custom support for the assignment
operator. Overloading the assignment operator for the Player class looks like this:

class Player
{
public:
Player();
Player(char* n, int s);
~Player();
Player& operator=(const Player& p);
void SetScore(int s);
int GetScore();
private:
char* pName;
int score;
};


Although it doesn’t look much like a member function declaration, it is. The syntax for overloading the
assignment operator involves adding a member function that uses a reference to the class type as an
argument, and another as a return type. The argument (in this case called p) is the input, or source object.
This is the object that will appear on the right side of the assignment operator. The return type is the new,
or destination object, as will appear on the left side of the assignment operator. The function name itself is
“operator=”, but the operator keyword isn’t required to invoke the assignment operator. Using this
special member function looks exactly like using the default C++ assignment operator:

Player p1, p2( “slowpoke”, 0 );

p1 = p2; // this invokes the new assignment operator

Because no special syntax is required, users for your class don’t have to remember to call a special
function to assign one Player object to another. Before we can use this code, we need to define the new
member function:

Player& Player::operator = (const Player& p)
{
score = p.score;

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 23 of 47
SetName( p.pName );

return *this;
}

The first line in the function body assigns the score data member using the score data member of the
object supplied as an argument. The next line calls the SetName function, using the pName data member
belonging to the source object. The SetName function, as shown earlier, performs the steps necessary to

discard any existing player name string, allocate a new one of the correct size, and assign the string
content with the new player name.

These two lines assign the state of the current object (the object to which this member function belongs)
to that of the source object provided as an argument. At this stage the assignment is complete the two
objects have the same values. But the assignment operator defines a return value that is a reference to the
newly assigned object. So the last step is to return a reference to the current object. This is where the this
pointer comes into play.

But this can’t be returned as it is. The function returns a reference, not a pointer, so the this pointer must
be de-referenced. This is confusing. How can de-referencing a pointer result in a reference? The term
reference has two meanings here. The de-reference operator retrieves the object at an address. This object
is exactly what we need to return. Providing a variable reference is simply a matter of providing the
variable itself—no special syntax is required. So we’re using the de-reference operator to ‘convert’ a
pointer to an object, and returning that.

With the assignment operator in place, we’re free to assign Player objects. Each assignment results in a
deep copy, so each variable remains completely autonomous, with it’s own memory allocated for the
storage of the player name.

Before we move on, let’s take another look at how the assignment operator is called. We know that our
overloaded assignment operator can be used just like the “normal” assignment operator, but it may not be
clear how this works, or whether our new function is being called at all. Here is the usage syntax again:

Player p1, p2( “slowpoke”, 0 );

p1 = p2;

In this example, p1 is the destination object. This object exists, having been declared on the previous line,
but is to be overwritten by the assignment. p2 is the source object. Its data will be unchanged by the

assignment. It is used in a read-only fashion only, which is why it can be provided to the destination
object in the form of a const reference. But how do we know that the custom assignment operator is really
being called? There are several ways to prove this. One is to add a cout to the function to display a
message like “it worked!” Another way is to call the assignment operator explicitly, like this:

p1.operator=(p2);

This code calls the assignment operator explicitly. There is no good reason to use this syntax, except to
verify that the assignment operator is present, and to clarify how the assignment operator works. This
syntax makes clear the fact that the assignment operator is indeed a Player class member function, which
in this case is invoked on the p1 object, using p2 as an argument. So, if the implementation of the
assignment operator seems strange, try revisiting it with this syntax in mind.

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 24 of 47
Overloaded Operators
The assignment operator is just one of many operators that C++ allows to be overridden. Another
example is the insertion operator (<<). The class upon which the global cout object is based, the ostream
class, overloads this operator to accept a host of different data types that can be displayed. Math-based
classes often override the addition, subtraction, multiplication, and division operators so that objects of
that type can be used inside formulas as though they were intrinsic types. The equality operators (== and
!=) can also be overloaded, allowing two objects of the same type to be compared.

Operator overloading is a powerful feature, but can be overused. Providing operators for a class is a
service if the operators in question have an intuitive purpose, but is a disservice otherwise. What would it
mean, for example, if the Player class supported the addition operator?

Player localPlayer, remotePlayer;

localPlayer = localPlayer + remotePlayer;


What does this code do? How can one player be added to another? Perhaps it means that the remote
player’s score is added to the local player’s score. Perhaps it means that the local player’s game resources
such as fuel or ammo are increased. This isn’t an intuitive operator for a class called Player.

When used correctly, operator overloading is great, but it has a high potential for code obfuscation.
Generally, operator overloading isn’t used extensively in games, and when it is, it is isolated to specific
sections of the code. Classes that provide math support, for example, are good candidates for operator
overloading. If there is an exception to this rule, it’s the assignment operator, whose meaning is
straightforward, and is useful for many different types of objects.
Copy Constructors
There is one additional type of constructor that is often useful: the copy constructor. This constructor is
very much like an assignment operator, which is why we waited until now to discuss it.

A copy constructor constructs an object based on another object, making a copy. Adding a copy
constructor to the Player class looks like this:

class Player
{
public:
Player();
Player(char* n, int s);
Player(const Player& p);
~Player();
Player& operator=(const Player& p);
void SetScore(int s);
int GetScore();
private:
char* pName;
int score;
};


The copy constructor is simply an overloaded constructor that takes a const reference to the same class. In
this sense it’s just like the constructor that takes the player name and score. The only difference is that the
copy constructor takes the input in more ready-to-use form. Copy constructors can be used like this:

www.gameinstitute.com Introduction to C and C++ : Week 3: Page 25 of 47

Player p1;
p1.SetName(“slowpoke”);

Player p2( p1 );

This code creates a copy of the p1 object by declaring another instance (class p2), and passing the source
object as an argument. p1 and p2 now have the same information. The copy constructor is defined this
way:

Player(const Player& p)
{
score = p.score;
pName = 0;
SetName( p.pName );
}

Like the assignment operator, the copy constructor uses the incoming object to assign the values of the
current object. In our case, however, there’s a little detail that requires attention: the pName data member
must be assigned to zero before SetName is called. This is because SetName uses the delete operator on
pName to release any previously allocated memory. This is a constructor, so no previous memory will be
present, but pName won’t be initialized either. We need to assign it to zero so that the delete operator
won’t be provided with an un-initialized pointer.


Notice that is wasn’t necessary to assign pName to zero when we wrote the assignment operator. This is
because the assignment operator is used for objects that already exist. A constructor has already been
called, so pName will be initialized, either to zero or to a valid string. In contrast, the copy constructor
must initialize its data members just like any other constructor, because the object is brand new when the
copy constructor is called.
The CmdLine Sample
The primary reason that we’ve used console applications thus far is because they are simple. But console
applications often play an important role in game programming: they are often used to implement custom
tools that assist in the game development process. In fact, some teams have programmers that are solely
dedicated to the development of tools—none of which are directly part of the game. Here are two
examples:

• Resource compilers - Games typically require a vast number of resource files that contain
bitmaps, 3D models, sound files, and level description files. There are sometimes thousands of
these files, and it’s risky to include them with the game executable in raw form, both because
they can be modified to create rogue versions of the game, and because they can be accidentally
deleted, leaving the game inoperable. The programming team often creates a console application
that combines these files into one large resource file that has a custom format.
• Test suites – It’s often the case that a complex portion of the game can be tested in isolation.
Console applications can be used to stress test non-graphical features, and generate log files with
the results. These test suites can be run regularly (perhaps nightly), and serve to alert the team
when bugs are introduced. These tools can also be used to track performance issues. The output
from these tools can be graphed over time, revealing trends and making it very easy to spot
changes that have unwanted effects on performance.

×