Classes
Classes are the most important addition to C++ and give the language its OO zeal. As I discussed before, a class is simply a container of both data and the methods (often called member functions) that operate on that data.
The New Struct in Town
Let's begin learning classes by starting with standard structures, with a little added twist. In C, you defined a structure like this:
struct Point
{
int x,y;
} ;
Then you could create an instance of a structure with this:
struct Point p1;
This creates an instance or object of the structure Point and names it p1. In C++, you don't need to use the struct keyword anymore to create an instance, hence:
Point p1;
This creates an instance of the structure Point named p1. The reason for this is that C++ programmers have been creating types so they didn't have to type struct anymore, like this:
typedef struct tagPOINT
{
int x,y;
} Point;
Thus the syntax
Point p1;
Classes are similar to the new structures in that you don't have to create a type. The definitions themselves are the types.
Just a Simple Class
A class in C++ is defined with the keyword class. Here's an example:
class Point
{
public:
int x,y;
} ;
Point p1;
This is almost identical to the struct version of Point; in fact, both versions of p1 work in the exact same way. For example, to access data, you just use the normal syntax:
p1.x = 5;
p1.y = 6;
And, of course, pointers work the same way. So, if you defined something like
Point *p1;
then you would have to allocate memory for it first with malloc() or new:
p1 = new Point;
Finally, you could assign values to x,y like this:
p1->x = 5;
p1->y = 6;
The bottom line is that classes and structures are identical when accessing public data elements. The key term is public—what does this mean? If you noticed in my previous example of the Point class, defined as
class Point
{
public:
int x,y;
} ;
the keyword public: appears at the top of the definition before any declarations. This defines the visibility of the variables (and member functions). There are a number of visibility options, but usually only two are used—public and private.
Public Versus Private
If you place the keyword public at the top of all your class definitions and have only data in the classes, you have nothing more than a standard structure. That is, structures are classes with public visibility. Public visibility means that anyone can look at the class data elements. As for code in the main, other functions, and member functions, the data is not hidden or encapsulated. Private visibility, on the other hand, lets you hide data that shouldn't be altered by other functions that aren't part of the class. For example, take a look at this class:
class Vector3D
{
public:
int x,y,z; // anyone can mess with these
private:
int reference_count; // this is hidden
} ;
Vector3D has two different parts: the public data area and the private data area. The public data area has three fields that can be changed by anyone: x,y,z. On the other hand, there is a hidden field called reference_count in the private section. This field is hidden to everything except the member functions of the class (there aren't any yet). Thus, if you were to write some code like this:
Vector3D v;
v.reference_count = 1; // illegal!
the compiler would give you an error! So the question is, what good are private variables if you can't access them? Well, they're great for writing something like a black box class when you don't want or need the user to alter internal working variables. In that example, private is the way to go. However, to access the private members, you need to add member functions or methods to the class—this is where we jump off the deep end.…
Class Member Functions (a.k.a. Methods)
A member function, or method (depending on whom you're talking to), is basically a function within a class that works only with the class. Here's an example:
class Vector3D
{
public:
int x,y,z; // anyone can mess with these
// this is a member function
int length(void)
{
return(sqrt(x*x + y*y + z*z);
} // end length
private:
int reference_count; // this is hidden
} ;
Notice the highlighted member function length(). I have defined a function right in the class! Weird, huh? Let's see how to use it:
Vector3D v; // create a vector
// set the values
v.x = 1;
v.y = 2;
v.z = 3;
// here's the cool part
printf("\nlength = %d",v.length());
You call a class member function just like you access an element. And if v were a pointer, you would do this:
v->length();
Now, you might be saying, "I have about 100 functions that are going to have to access the class data; I can't possibly put them all in the class!" Well, you can if you want, but I agree that it would get messy. However, you can define class member functions outside the class definition. We'll get to that in a minute. Right now, I want to add another member function to show you how you might access that private data member reference_count:
class Vector3D
{
public:
int x,y,z; // anyone can mess with these
// this is a member function
int length(void)
{
return(sqrt(x*x + y*y + z*z);
} // end length
// data access member function
void addref(void)
{
// this function increments the reference count
reference_count++;
} // end addref
private:
int reference_count; // this is hidden
} ;
You talk to reference_count via the member function addref(). This approach may seem odd, but if you think about it, it's a good thing. Now the user can't do anything stupid to the data member. It always goes through your access function, which in this case allows the caller only to increment the reference_count, as in
v.addref();
The caller can't change the reference count, multiply it by a number, and so on because reference_count is private. Only member functions of the class can access it—this is data hiding and encapsulation.
At this point, I think you're seeing the power of classes. You can fill them with data-like structure, add functions within the classes that operate on the data, and hide data—pretty cool! But it gets even better!
Constructors and Destructors
If you've been programming C for more than a week, there's something that I'm sure you've had to do about a million times—initialize a structure. For example, say that you create a structure Person like this:
struct Person
{
int age;
char *address;
int salary;
} ;
Person people[1000];
Now, you need to initialize 1,000 people structures. Maybe all you want to do is this:
for (int index = 0; index < 1000; index++)
{
people[index].age = 18;
people[index].address = NULL;
people[index].salary = 35000;
} // end for index
But what if you forget to initialize the data and then just use the structures? Well, you might wind up seeing your old friend General Protection Fault. Similarly, during the run of your program, what if you allocate memory and point the address field of a person to the memory like this?
people[20].address = malloc(1000);
And then you use the memory, forget about it, and do this:
people[20].address = malloc(4000);
Oops! A thousand bytes of memory just went to never-never land. What you needed to do, before allocating more memory, was release the old memory with a call to free() like this:
free(people[20].address);
I think you've probably made this mistake too. C++ solves these housekeeping problems by giving you two new automatic functions that are called when you create a class: constructors and destructors.
Constructors are called when a class object is instantiated. For example, when this code is executed:
Vector3D v;
the default constructor is called, which doesn't do anything in this case. And similarly, when v goes out of scope—that is, when the function that v was defined in terminates, or if v is global when the program terminates—the default destructor is called, which again doesn't do anything. To see any action, you have to write a constructor and destructor. You don't have to if you don't want to, and you can define one or both.
Writing a Constructor
Let's use the person structure converted to a class as an example:
class Person
{
public:
int age;
char *address;
int salary;
// this is the default constructor
// constructors can take a void, or any other set of parms
// but they never return anything, not even a void
Person()
{
age = 0;
address = NULL;
salary = 35000;
} // end Person
} ;
Notice that the constructor has the same name as the class, in this case Person. This is not a coincidence—it's a rule! Also, notice that the constructor returns nothing. This is a must. However, the constructor can take parameters. In this case, there are no parameters, but you can have constructors with parameters. In fact, you can have an infinite number of different constructors, each with a different calling list. This is how you can create various types of Persons with different calls. Anyway, to create a Person and have it automatically initialized, you just do this:
Person person1;
The constructor will be called automatically, and the following assignments will occur:
person1.age = 0;
person1.address = NULL;
person1.salary = 35000;
Cool, huh? Now, the power of the constructor comes into play when you code something like this:
Person people[1000];
The constructor will be called for every single instance of Person, and all 1,000 of them will be initialized without a single line of code on your part!
All right, now let's get a little more advanced. Remember how I told you that functions could be overloaded? Well, you can overload constructors too. Hence, if you wanted a constructor for which you could set the age, address, and salary during its creation, you could do this:
class Person
{
public:
int age;
char *address;
int salary;
// this is the default constructor
// constructors can take a void, or any other set of parms
// but they never return anything, not even void
Person()
{
age = 0; address = NULL; salary = 35000;
} // end Person
// here's our new more powerful constructor
Person(int new_age, char *new_address, int new_salary)
{
// set the age
age = new_age;
// allocate the memory for the address and set address
address = new char[strlen(new_address)+1];
strcpy(address, new_address);
// set salary
salary = new_salary;
} // end Person int, char *, int
} ;
Now you have two constructors, one that takes no parameters and one that takes three: an int, a char *, and another int. Here's an example of creating a person who is 24 years old, lives at 500 Maple Street, and makes $52,000 a year:
Person person2(24,"500 Maple Street", 52000);
Isn't that the coolest? Of course, you might think that you can initialize C structures as well with a different syntax, something like:
Person person = {24, "500 Maple Street", 52000} ;
However, what about the memory allocation? What about the string copying, and so on? Straight C can do a blind copy, but that's it. C++ gives you the power to also run code and logic when an object is created. This gives you much more control.
Writing a Destructor
After you've created an object, at some point it must die. This is where you might normally call a cleanup function in C, but in C++ the object cleans itself up via a call to its destructor. Writing a destructor is even simpler than writing a constructor because you have much less flexibility with destructors—they have only one form:
~classname();
No parameter, no return type—period. No exceptions! With this in mind, let's add a destructor to your Person class:
class Person
{
public:
int age;
char *address;
int salary;
// this is the default constructor
// constructors can take a void, or any other set of parms
// but they never return anything, not even void
Person()
{
age = 0; address = NULL; salary = 35000;
} // end Person
// here's our new more powerful constructor
Person(int new_age, char *new_address, int new_salary)
{
// set the age
age = new_age;
// allocate the memory for the address and set address
address = new char[strlen(new_address)+1];
strcpy(address, new_address);
// set salary
salary = new_salary;
} // end Person int, char *, int
// here's our destructor
~Person()
{
free(address);
} // end ~Person
} ;
I've highlighted the destructor. Notice that there's nothing special about the code within it; I could have done anything that I wanted. With this new destructor, you don't have to worry about deallocating memory. For example, in C, if you created a structure with internal pointers in a function and then exited the function without deallocating the memory pointed to by the structure, that memory would be lost forever. That's called a memory leak and is shown here with a C example:
struct
{
char *name;
char *ext;
} filename;
foo()
{
filename file; // here's a filename
file.name = malloc(80);
file.ext = malloc(4);
} // end foo
The structure file is destroyed, but the 84 bytes you allocated are lost forever! But in C++, with your destructor, this won't happen because the compiler makes sure to call the destructor for you, which deallocates the memory.
I've provided the basics about constructors and destructors, but there's a lot more. There are special constructors called copy constructors, assignment constructors, and so forth. But you have enough to get started. As for destructors, there's just one type, the one I showed you, so you're in good shape there.
|