Publishers of technology books, eBooks, and videos for creative people

Home > Articles > Web Design & Development > PHP/MySQL/Scripting

  • Print
  • + Share This
This chapter is from the book

Classes

To define objects in C++, you create classes. Defining your own classes is actually easier than explaining it. So rather than starting with a long explanation, let's go through the sample program in Listing 3.1.

Example 3.1. Defining a software object with a C++ class

1    #include <cstdlib>
2    #include <iostream>
3
4    using namespace std;
5
6    class my_object
7    {
8    public:
9        int i;
10       float f;
11   };
12
13   int main(int argc, char *argv[])
14   {
15       my_object anObject;
16
17       anObject.i = 10;
18        anObject.f = 3.14159;
19
20       cout << "i = " << anObject.i << endl;
21       cout << "f = " << anObject.f << endl;
22       system("PAUSE");
23       return (EXIT_SUCCESS);
24  }

The short program in Listing 3.1 demonstrates how to define a class in C++. It uses the C++ keyword class on line 6 to define a new type: my_object. Everything in the class definition must be inside the opening and closing braces. The definition of the contents of the my_object type spans lines 7–11.

This program uses the my_object type to declare a variable in the main() function on line 13 As you can see, declaring a variable of type my_object is done in just the same way as declaring integers or floating-point numbers. Just as with any other variable type, variables of type my_object can be accessed in the functions where they are declared. Another way to say that is that the rules of scope work the same way for both objects and built-in types such as int.

Member Data

The my_object type contains an integer called i and a floating-point number called f. Programs using the my_object type store data in i and f. Therefore, i and f are collectively referred to as the class's member data. Confusingly, when we talk about just i or f by themselves, we call them data members. In other words, one item of member data is called a data member. And I'll bet you thought programmers had no sense of humor.

A class's member data stores all of the information that describes the object the class represents. As mentioned earlier, that information is also called the class's attributes.

The member data in the my_object class in Listing 3.1 is defined as being public. This means that any function can access the member data directly. For example, on line 17, the main() function stores an integer in i. It does so by stating the variable name (anObject), followed by a period, and then the data member name (i).

After storing values in the member data of the variable anObject, the program in Listing 3.1 prints the values it stored. When it does, line 20 shows that it uses anObject.i just as if it was a regular int variable. Specifically, it uses the insertion operator to put the value in anObject.i into the stream cout. On line 21, it does the same with anObject.f. Figure 3.1 shows the output.

03fig01.jpg

Figure 3.1 The output of the program in Listing 3.1.

As you can see, the program printed the values in anObject.i and anObject.f in just the way it prints integer or floating-point variables.

You can declare more than one object of the same type in a function. Listing 3.2 shows how.

Example 3.2. Declaring two objects of the same type in a function

1    #include <cstdlib>
2    #include <iostream>
3
4    using namespace std;
5
6    class my_object
7    {
8    public:
9        int i;
10       float f;
11   };
12
13   int main(int argc, char *argv[])
14   {
15       my_object anObject, anotherObject;
16
17       anObject.i = 10;
18       anObject.f = 3.14159;
19
20       anotherObject.i = -10;
21       anotherObject.f = 0.123456;
22
23        cout << "anObject.i = " << anObject.i << endl;
24       cout << "anObject.f = " << anObject.f << endl;
25
26       cout << "anotherObject.i = " << anotherObject.i << endl;
27       cout << "anotherObject.f = " << anotherObject.f << endl;
28
29       system("PAUSE");
30       return (EXIT_SUCCESS);
31  }
32

As the program in Listing 3.2 illustrates, you can declare as many objects of the same type as you need. Each object has its own member data, in this case i and f. The value in anObject.i has nothing to do with the value in anotherObject.i; they are completely independent of each other. The same is true for anObject.f and anotherObject.f. You can see this from the output of the program, which appears in Figure 3.2 .

03fig02.jpg

Figure 3.2 The values in the two objects are independent of each other.

In Figure 3.2, the program stores and prints two different sets of values. This shows that the two objects contain different information; they are independent of each other.

Member Functions

As mentioned earlier, programmers define more than just the data for the objects they create. When a game programmer creates a type, she also writes the set of operations that can be performed on the type. Those operations are functions, which we introduced in chapter 2. The functions that form the set of valid operations that can be performed on a type are all members of the class. Just as we can define member data in classes, we can also define member functions. Member functions form the set of valid operations a program can perform on an object. Again, it's easier to show what I mean rather than launching into a long explanation. So let's examine the program in Listing 3.3.

Example 3.3. Creating and using member functions

1    #include <cstdlib>
2    #include <iostream>
3
4    using namespace std;
5
6    class my_object
7    {
8    public:
9        void SetI(int iValue);
10       int GetI();
11
12       void SetF(float fValue);
13       float GetF();
14
15   private:
16       int i;
17       float f;
18   };
19
20
21   void my_object::SetI(int iValue)
22   {
23       i = iValue;
24   }
25
26
27   int my_object::GetI()
28   {
29       return (i);
30   }
31
32
33   void my_object::SetF(float fValue)
34   {
35       f = fValue;
36   }
37
38
39   float my_object::GetF()
40   {
41       return (f);
42   }
43
44
45   int main(int argc, char *argv[])
46   {
47       my_object anObject, anotherObject;
48
49       anObject.SetI(10);
50       anObject.SetF(3.14159);
51
52       anotherObject.SetI(-10);
53       anotherObject.SetF(0.123456);
54
55       cout << "anObject.i = " << anObject.GetI() << endl;
56       cout << "anObject.f = " << anObject.GetF() << endl;
57
58       cout << "anotherObject.i = " << anotherObject.GetI() << endl;
59       cout << "anotherObject.f = " << anotherObject.GetF() << endl;
60
61       system("PAUSE");
62       return (EXIT_SUCCESS);
63  }

The program in Listing 3.3 is a modification of the program in Listing 3.2. If you compare the two programs, you'll see some very important changes.

First, the program in Listing 3.3 changed the member data from public to private. You'll see this on lines 15–17. Recall that any function in a program can access public member data. The same is true for public member functions. However, private members are different. The private member data on lines 16 and 17 can only be accessed by member functions. It's as if the class is a very exclusive club; only members can use the club's private goodies.

The list of member functions is shown on lines 9–13. Notice that all of these functions are public. This is typically how programmers define classes. You should only rarely make member data publicly accessible. Normally, the member functions should be public and the member data private.

The reason programmers make class data private and functions public is simple: control. If a class's member data can only be accessed by the member functions, then we're controlling how the data can be changed. If the member functions never let the data get into an invalid state, then the data will always be valid.

Remember our dragon example we used when we first started talking about classes? Recall that the dragon in the game could eat fireberries to spit fire farther. Suppose the number of fireberries the dragon has eaten is stored in the dragon class as a data member. What would happen in the game if that number somehow got to be negative? Oops. Now the dragon swallows fire rather than spits it. The game will probably crash. That's why it's vital to keep all data valid at all times.

When we define objects, controlling the access to member data helps keep the data in a valid state. No functions outside the class can change private data. As long as the member functions never let the data become invalid, the data is always in a valid state. This is such an important point that I'm going to emphasize it with a special Tip.

If you look back at Listing 3.3, you'll see the prototypes for the member functions on lines 9–13. Putting the prototypes in the class tells the compiler which functions are members. The member functions for this class are very simple: All they do is set or get the values of the member data. We'll see in later chapters that member functions can also check the values before they set the member data.

The actual functions are given on lines 21–42. Let's focus on the first line of the SetI() function, which appears on line 21. Notice that the first line of the function includes the name of the class followed by two colons. Two colons used together like that—known as the C++ scope resolution operator—is a way to tell the compiler, "This is the function SetI() that I said was a member in the my_object class definition." The compiler replies, "Aha. So every time I see a call to the my_object class's SetI() function, this is the function I'll use." All member functions must be written in this way.

The SetI() function sets the value of i. You might be thinking, "Duh. I could tell that from the name of the function." That's exactly why the function is named SetI(). When you write your member functions, name them in a way that anyone looking at your code can tell what the function does by its name. If you try to explain what the function does, "Duh" is a good response to get—it means you named your function well.

As I mentioned, the SetI() function sets the value of i to the value in the iValue parameter. In most programs, the SetI() function would check the value of iValue before saving it into i. I'll show how to do that sort of data validation when we discuss if-else statements in "The if-else Statement" section later in this chapter.

Now take a look at the main() function in Listing 3.3. On lines 49–50, you'll see calls to the SetI() and SetF() functions. Notice that the program calls member functions by stating the name of the object, followed by a period, and then the name of the function. There is an important reason for doing it this way: If you look on lines 52–53, you'll see two more calls to the SetI() and SetF() functions. The difference between these two pairs of function calls is that the first pair sets the member data in the anObject variable, while the second pair sets the member data of the anotherObject variable. So any time you call a member function, you call that function on an object. The function uses the member data of that particular object and no other. This enables you to specifically state which object you're changing.

Lines 55–59 demonstrate calls to the GetI() and GetF() functions. Again, they follow the same pattern as calls to the SetI() and SetF() functions. Specifically, they state the variable name, then a period, and then the function name. Your programs always call member functions in this way.

Constructors and Destructors

Classes can have special member functions called constructors and destructors. Constructors are called automatically when the object is created. Constructors are used to initialize the object's member data into known states before any other function can possibly use the object. Destructors are called automatically when the object is destroyed. They perform any cleanup tasks on the object that may be required. Listing 3.4 demonstrates the use of constructors and destructors.

Example 3.4. Constructors and destructors in classes

1    #include <cstdlib>
2    #include <iostream>
3
4    using namespace std;
5
6    class my_object
7    {
8    public:
9        my_object();
10       ~my_object();
11
12       void SetI(int iValue);
13       int GetI();
14
15       void SetF(float fValue);
16       float GetF();
17
18   private:
19       int i;
20       float f;
21   };
22
23
24   my_object::my_object()
25   {
26       cout << "Entering constructor." << endl;
27       i = 0;
28       f = 0.0;
29   }
30
31
32   my_object::~my_object()
33   {
34       cout << "Entering destructor." << endl;
35   }
36
37
38   void my_object::SetI(int iValue)
39   {
40       i = iValue;
41   }
42
43
44   int my_object::GetI()
45   {
46       return (i);
47   }
48
49
50   void my_object::SetF(float fValue)
51   {
52       f = fValue;
53   }
54
55
56   float my_object::GetF()
57   {
58       return (f);
59   }
60
61
62   int main(int argc, char *argv[])
63   {
64       my_object anObject, anotherObject;
65
66       anObject.SetI(10);
67       anObject.SetF(3.14159);
68
69       anotherObject.SetI(-10);
70       anotherObject.SetF(0.123456);
71
72       cout << "anObject.i = " << anObject.GetI() << endl;
73       cout << "anObject.f = " << anObject.GetF() << endl;
74
75       cout << "anotherObject.i = " << anotherObject.GetI() << endl;
76       cout << "anotherObject.f = " << anotherObject.GetF() << endl;
77
78       system("PAUSE");
79       return (EXIT_SUCCESS);
80   }

Looking at lines 9–10 of Listing 3.4, you'll see that the my_object class now contains prototypes for two additional member functions: the constructor and the destructor, respectively. These prototypes are very different from any that we have introduced so far. Unlike other member functions, the names of constructors exactly match the name of the class. The only difference between the names of a constructor and a destructor is that the destructor name is preceded by the tilde (pronounced TILL-duh) character, which looks like this: "~".

The code for the constructor is on lines 24–29. Like all member functions, the constructor must state the class that it's a member of by using the class name and then the scope resolution operator. That's why line 24 has the name of the class, then the two colons followed by the name of the constructor (which is also the name of the class).

Notice that neither the constructor nor the destructor has a return type. The constructor doesn't need a return type because it creates an object of the class that it's a member of. In Listing 3.4, the constructor creates an object of type my_object. In "programmer-speak," we say that the constructor's return type is implicit in its name.

Destructors, on the other hand, delete an object of their class type. There is no return type because there's nothing to return. The object is gone when the destructor ends.

When the constructor starts, it prints the message Entering constructor to the screen. Most constructors don't do this; I did it here just for demonstration purposes. The primary purpose of a constructor is to set the member data into a known state when the program creates an object of that type. That's exactly what happens on lines 27–28 of Listing 3.4. The constructor sets the member data to 0.

The destructor appears on lines 32–35. This class has nothing to clean up, so the destructor doesn't do anything significant. For demonstration purposes, it prints the message Entering destructor when it starts.

A look at the main() function on lines 64–79 shows that it is no different than the one in Listing 3.3. That probably seems strange. The logical question to ask is, "Why aren't the constructor and destructor called?" In fact, they are. The program output in Figure 3.3 proves it.

03fig03.jpg

Figure 3.3 Calling the constructor and destructor.

When I ran the program in Listing 3.4, it began with the main() function, as all C++ programs do. The first thing main() did was to declare two variables of type my_object. At that time, the program automatically called the constructor once for each object that main() created. The first two lines of the output in Figure 3.3 show that that's exactly what happened; the constructor was called twice. Each time the constructor executed, it printed the message Entering constructor.

On lines 66–70, main() set the member data in both of its my_object variables. It then printed the values in the member data. We can see that in the output in Figure 3.3 The program paused and waited for me to press a key. When I did (just in case you're curious, I pressed the letter Z), the program executed the return statement on line 79. This caused the main() function to end. When main() ended, the two variables it declared on line 64 were no longer being used. Programmers say that the variables went "out of scope." When variables go out of scope, C++ programs automatically calls the appropriate destructors once for each class variable. In this case, it called the my_object destructor twice, as you can see from the output in Figure 3.3

Inline Member Functions

As you begin to read professional C++ programming literature, you'll see that many programmers put the code for their member functions into the class definitions. This approach is allowed in C++ programming. Functions defined this way are called inline member functions. listing 3.5 demonstrates the use of inline member functions.

Example 3.5. A class with inline member functions

1    #include <cstdlib>
2    #include <iostream>
3
4    using namespace std;
5
6    class point
7    {
8    public:
9        point()
10       {
11           x = y = 0;
12       }
13
14       void SetX(int xValue)
15       {
16           x = xValue;
17       }
18
19       int GetX(void)
20       {
21           return (x);
22       }
23
24       void SetY(int yValue)
25       {
26           y = yValue;
27       }
28
29       int GetY(void)
30       {
31           return (y);
32       }
33
34   private:
35       int x, y;
36   };
37
38   int main(int argc, char *argv[])
39   {
40       point rightHere;
41
42       rightHere.SetX(10);
43        rightHere.SetY(20);
44
45       cout << "(x,y)=(" << rightHere.GetX();
46       cout << "," << rightHere.GetY() << ")";
47       cout << endl;
48
49       rightHere.SetX(20);
50       rightHere.SetY(10);
51
52       cout << "(x,y)=(" << rightHere.GetX();
53       cout << "," << rightHere.GetY() << ")";
54       cout << endl;
55
56       system("PAUSE");
57       return (EXIT_SUCCESS);
58   }

Rather than just prototypes for member functions, the class in Listing 3.5 contains the member functions themselves. All of the code for each function appears in the class. The main() function on lines 38–58 demonstrates that your programs can use inline member functions in exactly the same way they use member functions defined outside of a class.

You're probably quite logically wondering what, if any, differences exist between inline and out-of-line member functions. There's really only one difference: When you define a member function inline, the compiler substitutes the code from the inline member function into your program. With out-of-line functions, that doesn't happen. For instance, if you look back at Listing 3.5, you'll see the statement

rightHere.SetX(10);

on line 42. When this statement gets compiled, the compiler substitutes the code for the SetX() function right into the statement. That is, it puts the equivalent of

rightHere.x=10;

into the compiled program. Of course, it doesn't change the source code in the .cpp file. It performs the substitution in the object code that it generates. It does this everywhere in every function that calls the inline function. The program in Listing 3.5 makes one call to the constructor, and two calls each to SetX() and SetY(). That means it substitutes code from the member functions into the main() function five times.

When I was teaching college-level C++ programming classes, students would often ask, "Why not just make all functions inline?"

The first reason is that the compiler won't allow it. If the compiler determines that the function is too long or too complex, it won't compile the member function as an inline function. It still compiles the function without a problem. However, it converts the member function into an out-of-line function in the compiled code. And it does this without telling you. It is completely up to whoever writes the compiler whether or not a function can be made inline. You and I can't control it. Writing functions inline is more of a suggestion than a command.

Usually functions remain inline if all they do is set or get values from members in a class. They generally also remain inline if they perform simple calculations. However, member functions that contain loops or call other functions are not likely to remain inline.

Another reason why it might not be a good idea to make all member functions inline is that inline member functions can make programs very large. Because the C++ compiler performs code substitution with inline member functions, there are fewer function calls in programs so they run faster. However, it also makes them bigger because the code for the inline functions gets inserted repeatedly. Using inline functions means that there are lots of copies of the inline function in the compiled program. With out-of-line functions, the program jumps to the one and only copy of the member function. That's a tiny bit slower, but it makes the program smaller. So when you're writing programs, you have to decide which is more important—speed or size.

The last reason why you might not make all of your member functions inline is that it makes your classes hard to read. Complex classes with lots of member functions become huge when you use inline member functions. Other programmers tend to find them difficult to deal with. Plenty of programmers disagree with this point of view—you have to decide for yourself.

One way to have the advantages of inline member functions without cluttering up your class definitions is to use out-of-line inline functions.

Say what?

Amazingly enough, C++ lets you define out-of-line member functions that are inline. It sounds kooky, but it's actually a nice feature. listing 3.6 illustrates how to create out-of-line inline functions.

Example 3.6. Making out-of-line functions inline

1    #include <cstdlib>
2    #include <iostream>
3
4    using namespace std;
5
6    class point
7    {
8    public:
9        point();
10       void SetX(int xValue);
11       int GetX(void);
12       void SetY(int yValue);
13       int GetY(void);
14
15   private:
16       int x,y;
17    };
18
19
20    inline point::point()
21    {
22       x = y = 0;
23    }
24
25    inline void point::SetX(int xValue)
26    {
27       x = xValue;
28    }
29
30    inline int point::GetX(void)
31    {
32       return (x);
33    }
34
35    inline void point::SetY(int yValue)
36    {
37       y = yValue;
38    }
39
40    inline int point::GetY(void)
41    {
42       return (y);
43    }
44
45
46
47    int main(int argc, char *argv[])
48    {
49        point rightHere;
50
51        rightHere.SetX(10);
52        rightHere.SetY(20);
53
54        cout << "(x,y)=(" << rightHere.GetX();
55        cout << "," << rightHere.GetY() << ")";
56        cout << endl;
57
58        rightHere.SetX(20);
59        rightHere.SetY(10);
60
61        cout << "(x,y)=(" << rightHere.GetX();
62        cout << "," << rightHere.GetY() << ")";
63        cout << endl;
64
65        system("PAUSE");
66        return (EXIT_SUCCESS);
67    }

In Listing 3.6, all of the functions from Listing 3.5 are now moved out of line. Only the function prototypes remain in the class definition. However, if you look at the member functions, you'll see that each one begins with the C++ keyword inline. An example is the SetX() function on line 25. Putting the keyword inline at the beginning of the function definition makes SetX() an inline function even though the code appears out of line. Using this style gives you shorter, more readable class definitions and still provides the advantages of inline functions. When we examine the source code for the LlamaWorks2D game engine in the next chapter (the source code is also provided on the CD), you'll find that this is the style it uses for nearly all of its inline functions.

  • + Share This
  • 🔖 Save To Your Account