Other OO languages use different terminology than C++. Here are some equivalents:
OOP C++ Object Class object or instance Instance variable Private data member Method Public member function Message passing Calling a public member function
Within a class, all of the data and functions are related. Within a program, classes can be related in various ways.
An inheritance relationship can be represented by a hierarchy.
A partial vehicle hierarchy:
A partial animal hierarchy:
|
|
Another way to define the 3D struct so that we can reuse the Point2D struct:
struct Point3D_composite
{
Point2D xy_; // Struct contains a Point2D object
double z_;
};
The memory layout of Point3D and Point3D_composite is identical and
is obviously compatible with C, as there is nothing "C++" about them yet.
Accessing the members:
void PrintXY(const Point2D &pt)
{
std::cout << pt.x_ << ", " << pt.y_;
}
void PrintXYZ(const Point3D &pt)
{
std::cout << pt.x_ << ", " << pt.y_ << ", " << pt.z_;
}
void PrintXYZ(const Point3D_composite &pt)
{
std::cout << pt.xy_.x_ << ", " << pt.xy_.y_;
std::cout << ", " << pt.z_;
}
Of course, the last function can be modified to reuse PrintXY:
void PrintXYZ(const Point3D_composite &pt)
{
PrintXY(pt.xy_); // delegate for X,Y
std::cout << ", " << pt.z_;
}
A "cleaner" way to do define the 3D point is to use inheritance.
// Struct inherits a Point2D object
struct Point3D_inherit : public Point2D
{
double z_;
};
This new struct has the exact same physical structure as the previous two 3D point structs and
is still compatible with C:
|
|
Sample usage:
int main(void)
{
Point3D pt3;
Point3D_composite ptc;
Point3D_inherit pti;
char buffer[100];
sprintf(buffer, "pt3: x=%p, y=%p, z=%p\n", &pt3.x_, &pt3.y_, &pt3.z_);
std::cout << buffer;
sprintf(buffer, "ptc: x=%p, y=%p, z=%p\n", &ptc.xy_.x_, &ptc.xy_.y_, &ptc.z_);
std::cout << buffer;
sprintf(buffer, "pti: x=%p, y=%p, z=%p\n", &pti.x_, &pti.y_, &pti.z_);
std::cout << buffer;
// Assign to Point3D members
pt3.x_ = 1;
pt3.y_ = 2;
pt3.z_ = 3;
PrintXYZ(pt3);
std::cout << std::endl;
// Assign to Point3D_composite members
ptc.xy_.x_ = 4;
ptc.xy_.y_ = 5;
ptc.z_ = 6;
PrintXYZ(ptc);
std::cout << std::endl;
// Assign to Point3D_inherit members
pti.x_ = 7;
pti.y_ = 8;
pti.z_ = 9;
PrintXYZ(pti);
std::cout << std::endl;
return 0;
}
Output:
Notes about this syntax:pt3: x=0012FF68, y=0012FF70, z=0012FF78 ptc: x=0012FF50, y=0012FF58, z=0012FF60 pti: x=0012FF38, y=0012FF40, z=0012FF48 1, 2, 3 4, 5, 6 7, 8, 9
struct Point3D_inherit : public Point2D
|
|
Composite and inheritance (everything is public):
|
|
And in main we would have something that looks like this:
Point3D pt3; Point3D_composite ptc; Point3D_inherit pti; // setup points pt3.print(); ptc.print(); pti.print(); // Is this legal? Ambiguous? Which method is called?
Let's make it more C++-like with private members and public methods:
// Class is a stand-alone 2D point
class Point2D
{
public:
Point2D(double x, double y) : x_(x), y_(y) {};
void print(void)
{
std::cout << x_ << ", " << y_;
}
private:
double x_;
double y_;
};
// Class is a stand-alone 3D point
class Point3D
{
public:
Point3D(double x, double y, double z) : x_(x), y_(y), z_(z) {};
void print(void)
{
std::cout << x_ << ", " << y_ << ", " << z_;
}
private:
double x_;
double y_;
double z_;
};
With composition, we must initialize the contained Point2D object in the initializer list:
// Class contains a Point2D object
struct Point3D_composite
{
public:
Point3D_composite(double x, double y, double z) : xy_(x, y), z_(z) {};
void print(void)
{
xy_.print(); // 2D members are private
std::cout << ", " << z_;
}
private:
Point2D xy_;
double z_;
};
With inheritance, we must initialize the Point2D subobject in the
initializer list:
// Class inherits a Point2D object
struct Point3D_inherit : public Point2D
{
public:
Point3D_inherit(double x, double y, double z) : Point2D(x ,y), z_(z) {};
void print(void)
{
Point2D::print(); // 2D members are private
std::cout << ", " << z_;
}
private:
double z_;
};
Sample usage:
int main(void)
{
// Create Point3D
Point3D pt3(1, 2, 3);
pt3.print();
std::cout << std::endl;
// Create Point3D_composite
Point3D_composite ptc(4, 5, 6);
ptc.print();
std::cout << std::endl;
// Create Point3D_inherit
Point3D_inherit pti(7, 8, 9);
pti.print();
std::cout << std::endl;
return 0;
}
class Time
{
public:
Time(int h, int m, int s);
Time();
void Set(int h, int m, int s);
void Write(void) const;
void Increment(void);
private:
int hrs_;
int mins_;
int secs_;
};
A diagram of the Time class:
Note that sizeof(Time) is 12 bytes.
Partial implementation from Time.cpp: (Notice the code reuse even in this simple example.)
Time::Time()
{
Set(0, 0, 0);
}
Time::Time(int h, int m, int s)
{
Set(h, m, s);
}
void Time::Set(int h, int m, int s)
{
hrs_ = h;
mins_ = m;
secs_ = s;
}
Class Design Tip: When a class has more than one constructor, move the common initialization code into a separate private method whenever possible.
enum TimeZone {EST, CST, MST, PST, EDT, CDT, MDT, PDT};
We have several choices at this point:
Deriving ExtTime from Time:
class ExtTime : public Time
{
public:
ExtTime();
ExtTime(int h, int m, int s, TimeZone z);
void Set(int h, int m, int s, TimeZone z);
void Write(void) const;
private:
TimeZone zone_;
};
What is sizeof(ExtTime)? How might it be laid out in memory?
Some implementations of the ExtTime constructors:
The relationship between the Time and ExtTime classes:
- The derived class default constructor: (the base class default constructor is implicitly called)
ExtTime::ExtTime() { zone_ = EST; // arbitrary default }- The derived class non-default constructor: (the base class default constructor is implicitly called)
ExtTime::ExtTime(int h, int m, int s, TimeZone z) { zone_ = z; // what do we do with h, m, and s? }- Calling a non-default base class constructor explicitly:
ExtTime::ExtTime(int h, int m, int s, TimeZone z) : Time(h, m, s) { zone_ = z; }- Same as above using initializer list for derived member initialization:
Notes:ExtTime::ExtTime(int h, int m, int s, TimeZone z) : Time(h, m, s), zone_(z) { }
- The derived constructor calls the default base constructor if you don't call it explicitly.
- You can call any base constructor explicitly.
- A base constructor must be called from a derived constructor using the initializer list syntax. This is incorrect:
ExtTime::ExtTime(int h, int m, int s, TimeZone z) { Time(h, m, s); // Can't call a base constructor explicitly here zone_ = z; }- Key Point: A base constructor must be called, either implicitly or explicitly. If the base class has no default constructor, you must call another one explicitly. (If you don't, the compiler will generate an error.)
In the ExtTime class:
|
|
What is the result of this code:
ExtTime time(); time.Set(9, 30, 0); time.Write();
#ifndef EMPLOYEE_H
#define EMPLOYEE_H
const int MAX_LENGTH = 50;
class Employee
{
private:
char firstName_[MAX_LENGTH];
char lastName_[MAX_LENGTH];
float salary_;
int years_;
public:
Employee(const char* first, const char* last, float salary, int years);
void setName(const char* first, const char* last);
void setSalary(float newSalary);
void setYears(int numYears);
void Display(void) const;
};
#endif
A diagram of the Employee class:
What is sizeof(Employee)?
An implementation (Employee.cpp) for the Employee class:
#include "Employee.h"
#include <string>
#include <iomanip>
#include <iostream>
using namespace std;
Employee::Employee(const char* first, const char* last, float sal, int yrs)
{
strcpy(firstName_, first);
strcpy(lastName_, last);
salary_ = sal;
years_ = yrs;
}
void Employee::setName(const char* first, const char* last)
{
strcpy(firstName_, first);
strcpy(lastName_, last);
}
void Employee::setSalary(float newSalary)
{
salary_ = newSalary;
}
void Employee::setYears(int numYears)
{
years_ = numYears;
}
void Employee::Display(void) const
{
cout << " Name: " << lastName_;
cout << ", " << firstName_ << endl;
cout << setprecision(2);
cout.setf(ios::fixed);
cout << "Salary: $" << salary_ << endl;
cout << " Years: " << years_ << endl;
}
The specification (manager.h) for the Manager class:
#ifndef MANAGER_H
#define MANAGER_H
#include "employee.h"
class Manager : public Employee
{
private:
int deptNumber_; // department managed
int numEmployees_; // employees in department
public:
Manager(const char* first, const char* last, float salary, int years, int dept, int emps);
void setDeptNumber(int dept);
void setNumEmployees(int emps);
void Display(void) const;
};
#endif
A diagram of the Manager class:
What is sizeof(Manager)?
An implementation (Manager.cpp) for the Manager class:
#include "manager.h"
#include <iostream>
using namespace std;
Manager::Manager(const char* first, const char* last, float salary,
int years, int dept, int emps) : Employee(first, last, salary, years)
{
deptNumber_ = dept;
numEmployees_ = emps;
}
void Manager::Display(void) const
{
Employee::Display();
cout << " Dept: " << deptNumber_ << endl;
cout << " Emps: " << numEmployees_ << endl;
}
void Manager::setDeptNumber(int dept)
{
deptNumber_ = dept;
}
void Manager::setNumEmployees(int emps)
{
numEmployees_ = emps;
}
Trace the execution of the following program through the class hierarchy.
What is the output?
|
Output: Name: Doe, John Salary: $30000.00 Years: 2 Name: Smith, Mary Salary: $50000.00 Years: 10 Dept: 5 Emps: 8 Name: Jones, Mary Salary: $50000.00 Years: 10 Dept: 5 Emps: 8 Name: Jones, Mary Salary: $80000.00 Years: 10 Dept: 5 Emps: 10 |
Interface:
Implementation:const char *getFirstName(void) const; const char *getLastName(void) const; float getSalary(void) const; int getYears(void) const;
|
|
void Manager::LogActivity(void) const
{
char buffer[105];
const char *fn = getFirstName(); // use public accessor method
const char *ln = getLastName(); // use public accessor method
sprintf(buffer, "%s, %s", ln, fn); // Format lastname, firstname
cout << "============================" << endl;
cout << "Manager data:" << endl;
cout << " Dept: " << deptNumber_ << endl;
cout << " Emps: " << numEmployees_ << endl;
cout << "Employee data:" << endl;
cout << " Name: " << buffer << endl;
cout << " Salary: " << getSalary() << endl;
cout << " Years: " << getYears() << endl;
cout << "============================" << endl;
}
Some sample client code:
|
Output: ============================ Manager data: Dept: 7 Emps: 25 Employee data: Name: Faith, Ian Salary: 5 Years: 80000 ============================ |
class Employee
{
protected:
char firstName_[MAX_LENGTH];
char lastName_[MAX_LENGTH];
float salary_;
int years_;
public:
// Public methods...
};
How will this affect existing code, both derived classes and "global" code?
This will allow us to change the implementation of Manager to access the fields directly:
void Manager::LogActivity(void) const
{
char buffer[105];
const char *fn = firstName_; // direct access
const char *ln = lastName_; // direct access
sprintf(buffer, "%s, %s", ln, fn);
cout << "============================" << endl;
cout << "Manager data:" << endl;
cout << " Dept: " << deptNumber_ << endl;
cout << " Emps: " << numEmployees_ << endl;
cout << "Employee data:" << endl;
cout << " Name: " << buffer << endl;
cout << " Salary: " << salary_ << endl;
cout << " Years: " << years_ << endl;
cout << "============================" << endl;
}
Note that "regular" clients still must go through the public member functions:
|
#include "String.h"
class Employee
{
protected:
String firstName_; // String is a user-defined class
String lastName_; // String is a user-defined class
float salary_;
int years_;
public:
// Same public methods...
};
How will this affect existing code, both derived classes and "global" code?
class String
{
private:
char *data_; // the "real" C string
public:
String(); // default constructor
String(const String &str); // copy constructor
String(const char *str); // conversion constructor
~String(); // destructor
String & operator=(const String &str); // assigning another String
String & operator=(const char *str); // assigning a char *
const char *c_str(void) const; // get raw C string (data_)
// Other public methods...
};
const char *Employee::getFirstName(void) const
{
return firstName_.c_str(); // return as a C string
}
const char *Employee::getLastName(void) const
{
return lastName_.c_str(); // return as a C string
}
void Manager::LogActivity(void) const
{
char buffer[105];
const char *fn = firstName_; // direct access
const char *ln = lastName_; // direct access
// Other code...
}
because firstName_ and lastName_ are no longer C strings.
int main(void)
{
Manager m1("Ian", "Faith", 5, 80000, 7, 25);
// This is still OK because getLastName() was modified as well
const char *last2 = m1.getLastName();
return 0;
}