Published: August 2, 2008
By Richard G. Baldwin
File: AdvCpp00125
The purpose of this series of tutorial lessons is to help you to progress from procedural programming using C++ to object-oriented programming (OOP) using C++. Although there are several introductory lessons preceding this one, this is the first lesson in the series that introduces you to executable OOP code.
I recommend that you open another copy of this document in a separate browser window and use the following links to easily find and view the figures and listings while you are reading about them.
I recommend that you also study the other lessons in my extensive collection of online programming tutorials. You will find a consolidated index at www.DickBaldwin.com.
This lesson assumes that you already know something about structures and unions in the C programming language. The lesson introduces you to the C++ extensions to the structure and the union and also introduces you to the C++ capability embodied in the class.
C++ structures are similar to C structures but they have more features. When you define a structure in C++, you have designed a new data type and added it to the language. In C++, a structure is its own data type and can be known by its own name without the struct keyword.
C++ provides major support for the concept of encapsulation by extending the capability of the structure and introducing the class.
This lesson culminates with the presentation and explanation of an object-oriented Stack class.
A structure bundles together a set of data values of different types. For example, information about an employee could be stored in the structure shown in Figure 1.
Figure 1. A structure containing employee information.
struct employee { char last_name[30]; char first_name[20]; double employee_no; . . . float annual_salary; }; |
In C++, the code shown in Figure 1 defines a new data type named employee and specifies the list of data items (members) that a variable of type employee will have.
|
The example program shown in Listing 1 bundles together some of the information that a program would need to keep track of information about planets. Given the explanation in the sidebar, the expression mars.distance in Listing 1 is a reference to the structure's member containing the distance of Mars from the Sun in astronomical units.
Accessing fields of a structure
A field or member of a structure is referred to by using the structure name followed
by a period and the member name.
Listing 1. Source code for the program named Prog125B.
//File Prog125B /********************************************************* This program bundles together some of the information that a program would need to keep track of information about planets. *********************************************************/ #include <iostream> using namespace std; #include <string> struct planet{ char name[10]; float distance; float radius; }; planet mars; int main(){ strcpy(mars.name,"Mars"); mars.distance = 1.5; mars.radius = 0.4; cout << "Planetary statistics:\n"; cout << "Name: "<< mars.name << '\n'; cout << "Distance from Sun in AU: " << mars.distance << '\n'; cout <<"Radius in Earth radii: " << mars.radius << '\n'; //return 0; //Following statements cause Dev-C++ output to remain // on the screen system("PAUSE"); return EXIT_SUCCESS; }//end main |
A template for a new data type
Listing 1 begins by using the struct keyword to create a template for a new data type named planet. Then it creates a variable of type planet named mars. The variable named mars is manipulated in the main function.
The struct keyword gives the name planet to a structure consisting of three members or fields. The name field is an array of characters, while the distance and radius fields are floating-point values.
|
Declare a variable
The declaration
planet marsin Listing 1 creates a variable named mars whose type is planet.
Populate the members
Values are assigned to the three data members (fields) by code in the main function. Then the values stored in the three data members are displayed.
Notation for accessing data membersThe following notation
name.memberis used in Listing 1 to reference the individual data members of the structure for the purpose of storing as well as retrieving the values stored in those data members.
The screen output
Figure 2 shows the program output that appears on the command-line screen.
Figure 2. Output from program named Prog125B.
Planetary statistics: Name: Mars Distance from Sun in AU: 1.5 Radius in Earth radii: 0.4 Press any key to continue . . . |
As I mentioned earlier, this lesson begins the transition from procedural programming to object-oriented programming (OOP). This lesson also begins the introduction of important concepts that make C++ useful for object-oriented programming.
Models of the real world
OOP is a method of programming that seeks to mimic the way we form models of the real world. To cope with the complexities of life, we have evolved a capacity to generalize, classify, and generate abstractions. Many nouns in our vocabulary represent a class of objects sharing some set of attributes or behavioral traits. For example, from a world full of individual plants, we derive the abstract classes called fruit and vegetable. This allows us to develop and process ideas about fruits and vegetables without being distracted by the details concerning any fruit or vegetable. The OOP extensions in C++ exploit this natural tendency we have to classify and abstract things.
In addition to abstraction, an OOP program is characterized by three main characteristics:
This lesson will deal with encapsulation and defer inheritance and polymorphism until later in the series.
The word encapsulation derives from the word capsule. Most of us are familiar with capsules in a medical sense where a capsule is a container that contains medicine.
Encapsulation in C++ combines data and the functions used to manipulate that data in a single class-type object. For example, you might develop a data structure, such as an array holding the information needed to draw a character font on the screen, and the code (functions) for displaying, scaling, rotating, highlighting, and coloring your font character.
Different from procedural programming
In procedural programming, a common approach might be to put the data structures and related functions into a single, separately compiled source file in an attempt to treat code and data as a single module. While this is a step in the right direction, it isn't good enough. There is no explicit relationship between the data and the code, and you or another programmer can still access the data directly without using the functions provided. This can lead to problems.
For example, suppose that you decide to replace the array of font information with a linked list? Another programmer working on the same project may decide that he has a better way to access the font data, so he writes some functions of his own that manipulates the array directly. The problem is that the array isn't there any more after you replace the array with a linked list, so problems will ensue. You need a way to ensure that the data can be accessed only by calling functions that you provide.
C++ provides the class keyword
C++ extends the power of C's struct and union keywords by adding a keyword not found in C: class. Note that we must be careful to distinguish between class or classes as a concept, and class as a keyword. All three keywords are used in C++ to define classes in general. However, many C++ programmers prefer to use the keyword class to define a class and to use the keyword struct in its more conventional sense.
Defining and instantiating a new type
In C++, a single class entity (defined with the keywords struct, union, or class) combines functions (known as member functions) and data (known as data members). You usually give a class a useful name, such as Font. This name becomes a new type identifier that you can use to declare instances or objects of that class type. See the code fragment in Figure 3, for example, which shows the C++ syntax for defining a new type using the class keyword and declaring an object of the new type.
Figure 3. Code fragment for a Font class.
class Font{ //Declare your members here: both data and functions; //Don't worry about how for the moment. };//note the semicolon //Declares variable fancyFontVar to be of type Font Font fancyFontVar; |
The variable fancyFontVar is an instance (sometimes called an instantiation) of the class Font. The variable fancyFontVar is also an object.
Using the new object
You can use the class name Font very much like a normal C data type. For example, you can declare arrays and pointers as shown in Figure 4.
Figure 4. Declaring arrays and pointers of a class type.
Font anArray[10];//declare an array of 10 Font objects Font* font_ptr; //declare a pointer of type Font |
Differences between class, struct, union etc.
A major difference between C++ classes and C structures concerns the accessibility of members. The members of a C structure are freely available to any expression or function within their scope and there is nothing that you can do to protect those members from unauthorized access.
|
The members of a C++ structure are also, by default, freely available to any expression or function within their scope. However, with C++, you can control access to struct and class members (code and data) by declaring individual members as public, private, or protected. These three access levels will be explained in more detail later on.
Three keywords for defining classes
C++ structures and unions offer more than their C counterparts. They can hold function declarations and definitions as well as data members. In C++, the keywords struct, union, and class can all be used to define classes.
So, when we talk about classes in C++, we include structures and unions, as well at types defined by the keyword class. However, in this series of tutorials, I will probably concentrate mostly on classes declared using the keyword class and leave struct and union for others to discuss.
Good programming practice
Typically in OOP, you restrict data member access to member functions only. You usually make the data members private and the member functions that access those data members public.
Back to the Font class
Returning to the hypothetical problem of handling fonts (see Figure 3), how does the C++ class concept help? By creating a suitable Font class, you can ensure that the private font data members can be accessed and manipulated only through the public Font member functions that you have created for that purpose.
Free to change the implementation
You are then free at any time to change the font data structure from an array to a linked list, or whatever. You would, of course, need to re-code the member functions to handle the new font data structure, but if the function names and arguments are unchanged, programs (and programmers) in other parts of your system will be unaffected by your improvements.
Equal and interdependent partners
Thus, the technique of encapsulation in classes helps provide the very real benefit of modularity. The C++ class establishes a well-defined interface that helps you design, implement, maintain, and reuse programs. The class concept leads to the idea of data abstraction. Our font data structure is no longer tied to any particular physical implementation; rather, it is defined in terms of the operations (member functions) allowed on it. The C++ class combines data and function as equal, interdependent partners.
The class is the mechanism that is used to create objects. As such, the class is at the heart of many C++ features. The syntax of a class declaration using the keyword class is shown in Figure 5.
Figure 5. Syntax of a class declaration.
class class-name{ private functions and variables of the class public: public functions and variables of the class } optional object-list; |
An optional object list
Note that the object list in the class declaration is optional. You can declare class objects later, as needed. (Even if you omit the object list, however, you must include the semicolon.)
An optional class name
While the class-name is also technically optional, from a practical point of view, it is almost always needed. The reason for this is that the class-name becomes a new type name that is used to declare objects of the class.
Members of the class
Functions and variables declared inside a class declaration are said to be members of that class. By default, all functions and variables declared inside a class are private to that class (where the word class in this context means class and not struct, or union). This means that they are accessible only by other members of that class. The public keyword is used followed by a colon to declare public class members. All functions and variables declared after the public keyword are accessible both by other members of the class and by any other part of the program that contains or has access to the class.
A simple class declaration
Listing 2 shows a simple class declaration using the class keyword without the optional object list.
Listing 2. A simple class declaration.
class MyClass{ int data;//private to MyClass public: void setData(int num); int getData(); };//note the required semicolon |
|
This class has one private variable (data member), named data and two public member functions named setData and getData.
Note that functions are declared within a class using their prototype forms. (A complete definition of each prototype function must be provided elsewhere.) Functions that are declared to be part of a class are called member functions.
Access control
The variable named data is private. Therefore, it is not accessible by any code outside MyClass. However, because the two member functions are members of the class, they have direct access to data. The two member functions are declared as public members of the class. Therefore, they can be called by any other part of the program that contains or has access to the class.
Defining a member function
Although the two member functions are declared by the class in Listing 2, they are not yet defined. To define a member function, you must link the name of the class to which the member function belongs with the name of the function. You do this by preceding the function name with the class name and separating the two by two colons. The two colons are called the scope resolution operator. Listing 3 shows how the two member functions might be defined outside of the class declaration.
Listing 3. Syntax for defining member functions.
void MyClass::setData(int num){//note the two colons data = num; } // end setData int MyClass::getData(){ return data; }//end getData |
Because the two functions are member functions of the class, they have direct access to the private variable named data.
When you define a member function, use the general form shown in Figure 6 (unless it is defined as an inline function which will be explained later).
Figure 6. General syntax for defining a member function.
type class_name::func-name(parameter-list){ ... // body of function }//end of function |
Instantiating an object of the class
The declaration of MyClass in Listing 2 did not define any objects of type MyClass. The declaration and the associated function definitions simply defined the type of object that will be created when an object of the class is actually declared (instantiated). To instantiate an object, use the class name to specify the type. For example, the statement in Listing 4 declares (instantiates) two objects of type MyClass.
Listing 4. Declare two objects of type MyClass.
MyClass obj1, obj2; |
|
Accessing public members of an object
Once an object of a class has been instantiated, the program can reference its public members by using the dot (period) operator in conjunction with the name of the object.
For example, the code in Listing 5 calls the setData function belonging to objects obj11 and obj2 passing an int value as a parameter each time the function is called. Referring back to the definition of the setData method in Listing 3, we see that this causes the parameter value to be stored in the private variable named data belonging to the object on which the function is called.
Listing 5. Calling the setData method to store data in objects.
obj1.setData(10);// sets ob1's version of data to 10 obj2.setData(20);// sets ob2's version of data to 20 |
|
Instance variables
Each object contains its own copy of all data members declared in the class (except for the special case where the data member is declared as static, which will be discussed later). This means that the copy of data belonging to obj1 is distinct and different from the copy belonging to obj2.
Putting everything discussed so far into context, the sample program shown in Listing 6 uses MyClass described earlier to set the value of data for obj1 and obj2 and to display the value of data for each object.
Listing 6. Source code for the program named Prog125A.
//File Prog125A /********************************************************* This program declares a class named MyClass, defines functions for entering data into and getting data from a private variable named data, creates two instances of the class (objects), and manipulates those objects. *********************************************************/ #include <iostream> using namespace std; class MyClass{ int data;//variable is private by default public://everything following this is public //The following are function prototypes. //Public function to put a value into the variable data void setData(int num); //Public function used to get value stored in data int getData(); };//end of class declaration //------------------------------------------------------// //The following are function definitions. void MyClass::setData(int num){ data = num; }//end definition of setData function int MyClass::getData(){ return data; }//end definition of getData function int main(){ //declare two instances (objects) of the class MyClass obj1, obj2; //Store different values in each object.t. obj1.setData(10); obj2.setData(20); //Get and display the value stored in each object. cout << obj1.getData() << "\n"; cout << obj2.getData() << "\n"; //return 0; //Following statements cause Dev-C++ output to remain // on the screen system("PAUSE"); return EXIT_SUCCESS; }//end main |
You should be able to copy, compile, and execute the program shown in Listing 6 using Dev-C++. When you do, the values 10 and 20 should be displayed on separate lines on the command-line screen. If you want to use a tool other than Dev-C++, modify the statements at the end of the main method accordingly.
A class declaration can contain public member functions and can also contain public member variables. For example, the member variable named data could be accessed from any part of the program if the class were declared as shown in Listing 7.
Listing 7. Class with public member variable.
class MyClass { public: int data;//variable is public to this class //Function to enter a value into the variable data void setData(int num); //Function used to access the value of the variable data int getData(); };//end of class declaration |
|
In this case, there would be no need for the two member functions since their purpose is to provide an interface to the member variable data. Note however, that you would need to use the dot operator (obj1.data) to access the member variable.
It's time to get a real taste of the power of objects. For that, we will examine a more practical example. The program in Listing 18 creates a class named Stack that implements a stack that can be used to store and retrieve characters in a "last-in, first-out" (LIFO) fashion.
I will explain this program in fragments. Before getting into those details, however, I will present the screen output from the program shown in Figure 7 for reference later.
Figure 7. Screen output from the Stack program.
StackA is full StackB is full Pop stackA: c Pop stackB: C Pop stackA: b Pop stackB: B Pop stackA: a Pop stackB: A StackA is empty StackB is empty Press any key to continue . . . |
A LIFO stack capability
This program creates a Stack class that provides a LIFO
(Last In First Out) stack capability. If you are unfamiliar with a
LIFO container, it is essentially a container for data that lets you put data
in, and then take the data back out later in a specific order. When you remove a piece of data
from the stack, it will be the one most recently put into the container.
I have seen an analogy that compares a stack to the spring-loaded container that holds trays in some cafeterias. A cafeteria employee places clean trays in the container at the top. Customers remove trays from the top. When a customer removes a tray, it is the tray that was most recently put into the container by the cafeteria employee. If the cafeteria never runs out of clean trays, the first tray that was put in will remain at the bottom of the stack of trays forever and will never be used by a customer.
Two objects of a Stack class
In this program, two objects of the Stack class are instantiated and manipulated. This is a good example of OOP where the stacks are objects that can be manipulated by the program.
Compiler directives
The program begins in Listing 8 with the requisite compiler directives plus a directive to set the value of a constant named SIZE to 3. This constant will be used to specify the number of data values that can be stored in the stack before it overflows.
Listing 8. Compiler directives.
#include |
Declare a Stack class for storing char data
Listing 9 declares a class named Stack and declares two instance variables in the class. The instance variable named myStack is an array that will be used to store and retrieve the actual data that will be stored and retrieved from the stack. The instance variable named index will be used to keep track of the data inside the stack.
Listing 9. Declare a Stack class for storing char data.
class Stack{ //Create a char array to serve as the stack char myStack[SIZE]; //Create a variable to index within the stack int index; |
The instance variables that are declared in Listing 9 are private by default.
Declare three functions
Listing 10 provides the public prototypes of three functions that will be defined later outside the class declaration.
Listing 10. Declare three functions.
public: void init(); int push(char ch); char pop(); };//end of class declaration |
The function named init will be used to initialize an object instantiated from the Stack class.
The function named push will be used to store a value of type char in the stack. Note that push is a commonly used name for functions that perform this operation.
The function named pop will be used to remove a value from the stack. Also note that pop is a commonly used name for functions that perform this operation.
Listing 10 also signals the end of the class declaration.
Define the function used to initialize the stack
Listing 11 defines the function that is used to initialize the stack. Note the use of the scope resolution operator to associate this function definition with the function declaration provided in Listing 10.
Listing 11. Define the function used to initialize the stack.
void Stack::init(){ index = 0;//set the index to zero }//end init function |
The code in Listing 11 is straightforward and shouldn't require an explanation.
Define the function to push a character onto the stack
Listing 12 provides the definition for the push function declaration in Listing 10.
Listing 12. Define the function to push a character onto the stack.
int Stack::push(char ch){ if(index==SIZE){ return -1;//full }else{ myStack[index] = ch;//store indexed into the array index++;//increment the index return 0; }//end else }// end definition of push() |
The push method must prevent an actual stack overflow if the using program attempts to store data in excess of the capacity of the stack. If that happens, the code in Listing 12 simply refuses to store the data value and returns a value of negative 1 as a flag to the using program that a stack overflow error has occurred.
|
Otherwise, the new data value is stored in the array at the location specified by the current value of the private instance variable named index. Then the value of index is incremented so that the storage location of the next value will not be the same as the storage location of the current value.
Finally, the method returns a value of 0 to indicate that the storage of the value in the stack was successful.
Define the function to pop a character off the stack
Listing 13 defines the function that is used to remove a character from the stack.
Listing 13. Define the function to pop a character off the stack.
char Stack::pop(){ if(index == 0){ return -1;//empty }else{ index--;//decrement the index //Access the array and return the character return myStack[index]; }//end else }//end definition of pop() |
The pop function must deal with the possibility that the using program will attempt to remove a value from an empty stack. If this happens, the function simply returns a value of negative 1 to indicate that the stack is empty and the attempt was unsuccessful.
Otherwise, Listing 13 decrements the value of index to cause it to point to the location of the value that was most recently stored in the array. It uses the new value of index to access and return that value.
Removal from an array is not possible
It is not possible to physically remove a value from a C++ array, so the code in Listing 13 doesn't actually remove the value from the array. However, referring back to Listing 12, we see that the next call to the push function will cause the new value to be stored in the array location from which the value was retrieved in Listing 13, effectively replacing the old value by a new value.
Listing 13 also represents the end of the class definition for the class named Stack. At this point, all three of the function prototypes that were declared in Listing 10 have been defined.
Beginning of the main function
Listing 14 shows the beginning of the main function. The main function is designed to test the Stack class by creating and manipulating two objects of the class.
Listing 14. Beginning of the main function.
int main(){ Stack stackA, stackB;//create two objects of class Stack //Initialize the two stacks stackA.init(); stackB.init(); |
Listing 14 begins by declaring two variables of the Stack class named stackA and stackB. This instantiates two objects of the Stack class and makes them accessible via the names of the variables.
Then the code in Listing 14 uses the variable name in conjunction with the dot operator to call the init method on each object to initialize them.
Push data onto the two Stack objects
Listing 15 uses the variable names and the dot operator to call the push method several times in succession to push some data onto each of the two stacks. An attempt is made to overflow both stacks in order to test the capability of each object to prevent a physical overflow.
Listing 15. Push data onto the two Stack objects.
for(int cntr = 0;cntr < SIZE + 1;cntr++){ if(stackA.push('a' + cntr) == -1){ cout << "StackA is full\n"; }//end if if(stackB.push('A' + cntr) == -1){ cout << "StackB is full\n"; }//end if }//end for loop |
The code in Listing 15 causes the first two lines of text shown in Figure 7 to be displayed on the screen. These two lines of text are produced when the attempt is made to overflow each of the stacks.
Once the code in Listing 15 has finished execution, three characters have been stored in each stack and one character has been rejected for each stack.
Pop data values off of each stack
Listing 16 pops all of the data off of each stack and purposely attempts to pop more data than is currently stored in each stack.
Listing 16. Pop data values off of each stack.
for(int cnt=0;cnt < SIZE + 1;cnt++){ char data = stackA.pop(); if(data == -1){ cout << "StackA is empty\n"; }else{ cout << "Pop stackA: " << data << "\n"; }//end else data = stackB.pop(); if(data == -1){ cout << "StackB is empty\n"; }else{ cout << "Pop stackB: " << data << "\n"; }//end else }//end for loop |
Each value is displayed as it is popped as shown in Figure 7. When all of the data in the stack has been popped and another attempt is made to pop a value, the pop method returns a value of negative 1 causing the empty messages shown in Figure 7 to be displayed.
End of the main function
Listing 17 shows the end of the main function and the end of the program.
Listing 17. End of the main function.
//return 0; //Following statements cause Dev-C++ output to remain // on the screen system("PAUSE"); return EXIT_SUCCESS; }//end main |
Listing 17 includes the two statements that are required to cause the Dev-C++ output to remain on the command-line screen so that you can view it. If you run this program with some other C++ tool, you may need to delete these two statements and re-enable the standard return statement shown in Listing 17.
This lesson introduced you to the C++ extensions to the structure and the union. It also introduced you to the class keyword and the C++ capability embodied in the class.
As a capstone, the lesson presented and explained an object-oriented Stack class.
Listing 18. Source code for the Stack program.
//File Prog125C.cpp 08/01/08 /********************************************************* This example creates a Stack class that provides a LIFO (Last In First Out) stack capability. Two objects of the Stack class are instantiated and manipulated. This is a good example of OOP where the stacks are objects that can be manipulated by the program. *********************************************************/ #include <iostream> using namespace std; #define SIZE 3 //used to control the size of the stack //Declare a Stack class for storing char data class Stack{ //Create a char array to serve as the stack char myStack[SIZE]; //Create a variable to index within the stack int index; public: void init();//function to initialize the stack //Function to push characters onto the stack int push(char ch); char pop();//function to pop characters off the stack };//end of class declaration //------------------------------------------------------// //Define the function used to initialize the stack void Stack::init(){ index = 0;//set the index to zero }//end init function //Define the function to push a character onto the stack int Stack::push(char ch){ if(index==SIZE) { return -1;//full }else{ myStack[index] = ch;//store indexed into the array index++;//increment the index return 0; }//end else }// end definition of push() //Define the function to pop a character off the stack char Stack::pop(){ if(index == 0){ return -1;//empty }else{ index--;//decrement the index //Access the array and return the character return myStack[index]; }//end else }//end definition of pop() //------------------------------------------------------// //Test the program by creating and manipulating two // objects of class Stack int main(){ Stack stackA, stackB;//create two objects of class Stack //Initialize the two stacks stackA.init(); stackB.init(); //Push some data onto the two stacks. Purposely try to // overflow both stacks. for(int cntr = 0;cntr < SIZE + 1;cntr++){ if(stackA.push('a' + cntr) == -1){ cout << "StackA is full\n"; }//end if if(stackB.push('A' + cntr) == -1){ cout << "StackB is full\n"; }//end if }//end for loop // Now pop the values off the two stacks. Purposely // attempt to pop more data than exists in the stacks. for(int cnt=0;cnt < SIZE + 1;cnt++){ char data = stackA.pop(); if(data == -1){ cout << "StackA is empty\n"; }else{ cout << "Pop stackA: " << data << "\n"; }//end else data = stackB.pop(); if(data == -1){ cout << "StackB is empty\n"; }else{ cout << "Pop stackB: " << data << "\n"; }//end else }//end for loop //return 0; //Following statements cause Dev-C++ output to remain // on the screen system("PAUSE"); return EXIT_SUCCESS; }//end main |
Copyright 2008, Richard G. Baldwin. Reproduction in whole or in part in any form or medium without express written permission from Richard Baldwin is prohibited.
Richard has participated in numerous consulting projects and he frequently provides onsite training at the high-tech companies located in and around Austin, Texas. He is the author of Baldwin's Programming Tutorials, which have gained a worldwide following among experienced and aspiring programmers. He has also published articles in JavaPro magazine.
In addition to his programming expertise, Richard has many years of practical experience in Digital Signal Processing (DSP). His first job after he earned his Bachelor's degree was doing DSP in the Seismic Research Department of Texas Instruments. (TI is still a world leader in DSP.) In the following years, he applied his programming and DSP expertise to other interesting areas including sonar and underwater acoustics.
Richard holds an MSEE degree from Southern Methodist University and has many years of experience in the application of computer technology to real-world problems.
-end-