Thứ Hai, 10 tháng 2, 2014
Tài liệu Giáo trình C++ P3 pdf
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
Đăng ký:
Đăng Nhận xét (Atom)
Không có nhận xét nào:
Đăng nhận xét