Giáo trình C++ P3

Chia sẻ: Hoang Nguyen | Ngày: | Loại File: PDF | Số trang:0

0
61
lượt xem
28
download

Giáo trình C++ P3

Mô tả tài liệu
  Download Vui lòng tải xuống để xem tài liệu đầy đủ

Introduction to C++

Chủ đề:
Lưu

Nội dung Text: Giáo trình C++ P3

  1. Week 3 Game Institute Introduction to C and C++ by Stan Trujillo www.gameinstitute.com Introduction to C and C++ : Week 3: Page 1 of 47
  2. © 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 2 of 47
  3. 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 3 of 47
  4. 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 4 of 47
  5. 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 5 of 47
  6. 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 6 of 47
  7. 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 7 of 47
  8. 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 8 of 47
  9. { 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 9 of 47
  10. 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
  11. // 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 11 of 47
  12. 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 12 of 47
  13. 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 13 of 47
  14. 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 14 of 47
  15. 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 15 of 47
  16. 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 16 of 47
  17. 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 17 of 47
  18. // 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 18 of 47
  19. 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 19 of 47
  20. 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 20 of 47
Đồng bộ tài khoản