Preface
This article will step through some of the basics of C++ and object-oriented programming with the intention of using C++ in a practical application. The original C++ syntax is based on the C language. Problems, however, arose from the fact that C++ programmers would sometimes use another developer’s class library that defined the members differently in a class of the same name. This resulted in system crashes and the introduction to the concept of the namespace.
The ANSI/ISO C language is a procedurally-oriented language and as such, uses the function as the basic unit of organization. The functions contained in a C program are predefined to reside in header files. The header files do not get compiled, but rather are preprocessed. The source code is translated into native machine language into an object file. The executable is built during the linking process, where the linker copies the function definitions contained in those included header files onto the translated source code file. C++ is an object-oriented language that uses the class as its basic unit of organization. The header files therefore consist of class libraries. A class is an abstraction. For example, a class Orange
may represent one box of oranges. The oranges in that box are the members. The methods used to operate on those oranges are semantically-related. The number of object members is concretely defined. The class is an abstraction to provide a conceptual view. You make a new type in C++ by declaring a class. A class is just a collection of data members – often of different types – combined with a set of functions. In C++, a header file declares those classes that contain object members. The source code files implement those methods defined in the class on the data members of that class. An object is never accessed as a whole. Access to an object by instantiating a class involves three keywords: public
, private
, and protected
. Here is an example of declaring a class:
class Dog
{
unsigned int itsAge;
unsigned int itsWeigth;
void Bark();
}
For the beginner, <iostream>
is the header file for the input and output stream to and from standard output. “cout
” means console output, and “cin
” mean console input. It is best to use the using namespace std;
statement to indicate that you are importing the Standard Template Library. Declaring this class doesn’t allocate memory for a Dog
. It just tells the compiler what a Dog
is, what data members it contains (itsAge
, itsWeight
), and what it can do (Bark()
). Now, while memory was not allocated, it does let the compiler know that how big a Dog
is (i.e., how much room the compiler must set aside for each Dog
that we create). In this example, if an integer (Int32
) is four bytes, a Dog
then is 8 bytes big: itsAge
is 4 bytes and itsWeight
is 4 bytes. Bark()
takes up only the room required for storing information on the location of Bark()
. This is a pointer to a function that can take 4 bytes on a 32 bit platform.
Public versus Private
The code above does not use any of the accessor keywords, and is therefore considered to be private by default. Private members can be accessed only within the methods of the class itself. Public members can be accessed through any object of the class. The way to use Dog
so that you can access the data members is to make some of the members public:
class Dog
{
Public:
unsigned int itsAge;
unsigned int itsWeigth;
void Bark();
};
Now, we will write code that demonstrates the declaration of a Dog
with public member variables:
#include <iostream>
class Dog
{
public:
int itsAge;
int itsWeigth;
void Bark();
};
int main() {
{
Dog Lassie;
Lassie.itsAge = 5; std::cout << "Lassie is a dog who is " ;
std::cout << Lassie.itsAge << " years old.\n";
return 0;
}
}
Compiling this code results in the phrase: Lassie is a dog who is 5 years old.
When designing types, you should keep the data members of a class private. To access private data in a class, you must create public functions known as accessor methods. Use these methods to set and get the private member variables. These accessor methods are the member functions that other parts of your program call to get and set private member variables: a public accessor method is, then, a class member function used either to read (get) the value of a private class member variable or to set its value. This leads us to a standardized mechanism that is used in object-oriented programming: separation of the interface from its implementation. Accessor functions are added rather than reading data directly. This creates indirection, but this indirection means that accessor functions enable you to separate the details of how data is stored from how it is used. By using accessor functions, you can later change how the data is stored without having to rewrite any of the other functions used in your application that use the data. This is why an object is never accessed as a whole, and this is a major tenet of COM programming’s language independence: the separation of interface from its implementation hides the details of how the functionality is carried out. COM is a binary entity, and certain languages define the same function differently. If the interface definition is universal, then at the binary level, where all memory tables will appear the same, the function definitions can reside, so long as the language uses functions that return function pointers. Therefore, an accessor function provides a public interface to the private data members of the class. Examine the code below that illustrates the complete declaration of a class and the implementation of its accessor functions and one general class member function:
#include <iostream>
using namespace std;
class Dog
{
public:
int GetAge();
void SetAge(int age);
void Bark();
private:
int itsAge;
};
int Dog::GetAge()
{
return itsAge;
}
void Dog::SetAge(int age)
{
itsAge = age;
}
void Dog::Bark()
{
std::cout << "Bark.\n";
}
int main()
{
Dog Lassie;
Lassie.SetAge(5);
Lassie.Bark();
std::cout << "Lassie is a dog who is " ;
std:cout << Lassie.GetAge() << " years old.\n";
Lassie.Bark();
return 0;
}
The output is, of course:
Bark.
Lassie is a dog who is 5 years old.
Bark.
Adding Constructors and Destructors
A data-type, or variable, is always first declared, and then initialized. For example, ‘int Weight
” declares the variable of type integer , which tells the compiler 4 bytes of storage needs to be set aside. When we initialize the variable, we assign it a value: Weight = 7;
. Of course, we can both declare and initialize a variable: int Weight = 7;
. You can initialize the member data of a class using a special member called a constructor. The constructor can take parameters as needed, but they cannot have a return value (not even void
). The constructor is a class method with the same name as the class itself. Whenever you declare a constructor, you’ll also want to declare a destructor. Just as constructors create and initialize objects of a class, destructors clean up after your object is not being used anymore by an object user. That is, a destructor purposes to free up the resources and any memory that you might have allocated (either in the constructor or throughout the lifespan of the object). Destructors always have the same name as the class, preceded by the tilde (~): ~Dog()
. Here is code that illustrates these operations:
#include <iostream> // for cout
class Dog {
public: Dog(int initialAge); ~Dog(); int GetAge(); void SetAge(int age); void Bark();
private: int itsAge; };
Dog::Dog(int initialAge)
{
itsAge = initialAge;
}
Dog::~Dog() {
}
int Dog::GetAge()
{
return itsAge;
}
void Dog::SetAge(int age)
{
itsAge = age;
}
void Dog::Bark()
{
std::cout << "Bark.\n";
}
int main()
{
Dog Lassie(5);
Lassie.Bark();
std::cout << "Lassie is a dog who is " ;
std::cout << Lassie.GetAge() << " years old.\n";
Lassie.Bark();
Lassie.SetAge(7);
std::cout << "Now Lassie is " ;
std::cout << Lassie.GetAge() << " years old.\n";
return 0;
}
Output
Bark.
Lassie is a dog who is 5 years old.
Bark.
Now Lassie is 7 years old.
A Practical Application
Whether you are using Visual Studio 2005 or 2008, download the zip files, extract all of the files into a newly made folder in your project's directory, and double-click the solution file. The enclosed application can be expanded upon and changed to suit your needs. It is a console-based application that manages an entities Employee records, and provides the ability to either hire or fire an employee, or list any past, current, and present employee. Other properties like salary and ID are included. Shown below is the Employee.h header file. Since the Employee
class maintains all of the information about an employee, its methods provide a way to query and change that information.
#include <iostream>
namespace Records {
const int kDefaultStartingSalary = 30000;
class Employee
{
public:
Employee();
void promote(int inRaiseAmount = 1000);
void demote(int inDemeritAmount = 1000);
void hire(); void fire(); void display();
void setFirstName(std::string inFirstName);
std::string getFirstName();
void setLastName(std::string inLastName);
std::string getLastName();
void setEmployeeNumber(int inEmployeeNumber);
int getEmployeeNumber();
void setSalary(int inNewSalary);
int getSalary();
bool getIsHired();
private:
std::string mFirstName;
std::string mLastName;
int mEmployeeNumber;
int mSalary;
bool fHired;
};
}
The Employee.h file determines the behavior of the Employee
class. The Records
namespace is used throughout the program for application-specific code. A number of accessors provide mechanisms to change the information about an employee or query the current information about an employee. The data members are declared as private so that other parts of the code cannot modify them directly. The accessors provide the only public way of modifying or querying these values. The Employee.cpp file illustrates the implementations of the Employee
class’s methods:
#include <iostream>
#include <string>
#include "Employee.h"
using namespace std;
namespace Records {
Employee::Employee()
{
mFirstName = "";
mLastName = "";
mEmployeeNumber = -1;
mSalary = kDefaultStartingSalary;
fHired = false;
}
void Employee::promote(int inRaiseAmount)
{
setSalary(getSalary() + inRaiseAmount);
}
void Employee::demote(int inDemeritAmount)
{
setSalary(getSalary() - inDemeritAmount);
}
void Employee::hire()
{
fHired = true;
}
void Employee::fire()
{
fHired = false;
}
void Employee::display()
{
cout << "Employee: " << getLastName() <<
", " << getFirstName() << endl;
cout << "-------------------------" << endl;
cout << (fHired ? "Current Employee" : "Former Employee") << endl;
cout << "Employee Number: " << getEmployeeNumber() << endl;
cout << "Salary: $" << getSalary() << endl;
cout << endl;
}
void Employee::setFirstName(string inFirstName)
{
mFirstName = inFirstName;
}
string Employee::getFirstName()
{
return mFirstName;
}
void Employee::setLastName(string inLastName)
{
mLastName = inLastName;
}
string Employee::getLastName()
{
return mLastName;
}
void Employee::setEmployeeNumber(int inEmployeeNumber)
{
mEmployeeNumber = inEmployeeNumber;
}
int Employee::getEmployeeNumber()
{
return mEmployeeNumber;
}
void Employee::setSalary(int inSalary)
{
mSalary = inSalary;
}
int Employee::getSalary()
{
return mSalary;
}
bool Employee::getIsHired()
{
return fHired;
}
}
The EmployeeTest.cpp file is used to test some of the operations. If you are confident that the Employee
class works, then you, as shown, comment out the code:
#include <iostream>
#include "stdafx.h"
#include "Employee.h"
using namespace std;
The Database
class uses an array to store Employee
objects. An integer called mNextSlot
is used as a marker to keep track of the next unused array slot. This is not a good use of data structures because the array is fixed size -- it does not dynamically resize:
#include <iostream>
#include "Employee.h"
namespace Records {
const int kMaxEmployees = 100;
const int kFirstEmployeeNumber = 1000;
class Database
{
public:
Database();
~Database();
Employee& addEmployee(std::string inFirstName, std::string inLastName);
Employee& getEmployee(int inEmployeeNumber);
Employee& getEmployee(std::string inFirstName, std::string inLastName);
void displayAll();
void displayCurrent();
void displayFormer();
protected:
Employee mEmployees[kMaxEmployees];
int mNextSlot;
int mNextEmployeeNumber;
};
}
Two constants are associated with the database. The maximum number of employees is a constant because the records are kept in a fixed-size array. Because the database will also take care of automatically assigning an employee number to a new employee, a constant defines where the numbering begins. The database provides an easy way to add a new employee by providing a first and last name. For convenience, this method will return a reference to the new employee. Below is the corresponding Database.cpp file:
#include <iostream>
#include <stdexcept>
#include <string>
#include "Database.h"
using namespace std;
namespace Records {
Database::Database()
{
mNextSlot = 0;
mNextEmployeeNumber = kFirstEmployeeNumber;
}
Database::~Database()
{
}
Employee& Database::addEmployee(string inFirstName, string inLastName)
{
if (mNextSlot >= kMaxEmployees) {
cerr << "There is no more room to add the new employee!" << endl;
throw exception();
}
Employee& theEmployee = mEmployees[mNextSlot++];
theEmployee.setFirstName(inFirstName);
theEmployee.setLastName(inLastName);
theEmployee.setEmployeeNumber(mNextEmployeeNumber++);
theEmployee.hire();
return theEmployee;
}
Employee& Database::getEmployee(int inEmployeeNumber)
{
for (int i = 0; i < mNextSlot; i++) {
if (mEmployees[i].getEmployeeNumber() == inEmployeeNumber) {
return mEmployees[i];
}
}
cerr << "No employee with employee number "
<< inEmployeeNumber << endl;
throw exception();
}
Employee& Database::getEmployee(string inFirstName, string inLastName)
{
for (int i = 0; i < mNextSlot; i++) {
if (mEmployees[i].getFirstName() == inFirstName &&
mEmployees[i].getLastName() == inLastName) {
return mEmployees[i];
}
}
cerr << "No match with name " << inFirstName
<< " " << inLastName << endl;
throw exception();
}
void Database::displayAll()
{
for (int i = 0; i < mNextSlot; i++) {
mEmployees[i].display();
}
}
void Database::displayCurrent()
{
for (int i = 0; i < mNextSlot; i++) {
if (mEmployees[i].getIsHired()) {
mEmployees[i].display();
}
}
}
void Database::displayFormer()
{
for (int i = 0; i < mNextSlot; i++) {
if (!mEmployees[i].getIsHired()) {
mEmployees[i].display();
}
}
}
}
The Database
class’s DatabaseTest.cpp file is shown below, again with the main body of the source code execution commented out. Classes are best tested in isolation:
#include <iostream>
#include "Database.h"
using namespace std;
using namespace Records;
The User Interface
The UserInterface.cpp file provides a menu-based display in which to work with. The main function is a loop that displays the menu, performs the selected action, then does it all again. For most actions, separate functions are defined. For simpler actions, like displaying employees, the actual code is put in the appropriate case.
#include <iostream>
#include <stdexcept>
#include <string>
#include "Database.h"
using namespace std;
using namespace Records;
int displayMenu();
void doHire(Database& inDB);
void doFire(Database& inDB);
void doPromote(Database& inDB);
void doDemote(Database& inDB);
int main(int argc, char** argv)
{
Database employeeDB;
bool done = false;
while (!done) {
int selection = displayMenu();
switch (selection) {
case 1:
doHire(employeeDB);
break;
case 2:
doFire(employeeDB);
break;
case 3:
doPromote(employeeDB);
break;
case 4:
employeeDB.displayAll();
break;
case 5:
employeeDB.displayCurrent();
break;
case 6:
employeeDB.displayFormer();
break;
case 0:
done = true;
break;
default:
cerr << "Unknown command." << endl;
}
}
}
int displayMenu()
{
int selection;
cout << endl;
cout << "Employee Database" << endl;
cout << "-----------------" << endl;
cout << "1) Hire a new employee" << endl;
cout << "2) Fire an employee" << endl;
cout << "3) Promote an employee" << endl;
cout << "4) List all employees" << endl;
cout << "5) List all current employees" << endl;
cout << "6) List all previous employees" << endl;
cout << "0) Quit" << endl;
cout << endl;
cout << "---> ";
cin >> selection;
return selection;
}
void doHire(Database& inDB)
{
string firstName;
string lastName;
cout << "First name? ";
cin >> firstName;
cout << "Last name? ";
cin >> lastName;
try {
inDB.addEmployee(firstName, lastName);
} catch (std::exception ex) {
cerr << "Unable to add new employee!" << endl;
}
}
void doFire(Database& inDB)
{
int employeeNumber;
cout << "Employee number? ";
cin >> employeeNumber;
try {
Employee& emp = inDB.getEmployee(employeeNumber);
emp.fire();
cout << "Employee " << employeeNumber
<< " has been terminated." << endl;
} catch (std::exception ex) {
cerr << "Unable to terminate employee!" << endl;
}
}
void doPromote(Database& inDB)
{
int employeeNumber;
int raiseAmount;
cout << "Employee number? ";
cin >> employeeNumber;
cout << "How much of a raise? ";
cin >> raiseAmount;
try {
Employee& emp = inDB.getEmployee(employeeNumber);
emp.promote(raiseAmount);
} catch (...) {
cerr << "Unable to promote employee!" << endl;
}
}
Here is a view of the console application, with its options: