Part B - Foundations
Member Functions and Privacy
Introduce member functions into the definition of a structure
Control accessibility to the data members within an instance of a structure
Introduce the concept of an object's empty state
"Because different structures can
have member functions with the same name, we must
specify the structure name when defining a member
function" (Stroustrup, 1997)
Member Functions
| Privacy
| Empty State
| Inlining
| Summary
| Exercises
A major enhancement that the C++ language introduced to C was
to include logic within the description of a compound type.
C++ lets us add functions as members of a compound type, alongside its
data members. While data members hold information about an
object's state, member functions define the operations performable
on that state.
This chapter describes the syntax for declaring and defining member functions
and for limiting accessibility to select members of a compound type.
Member Functions
Member functions provide the communication links between a client and an
instance of a compound type. The client calls the compound type's
member functions to access and manipulate the data within an instance of
the compound type.

We classify member functions under two mutually exclusive categories:
- query - report the state of the object
- modifier - change the state of the object

Every member function has direct access to all of the members of its
compound type. It receives and passes information between the
client and the object through its parameters and return value.
For example, consider a Student
type that is defined by
struct Student {
int no;
char grade[14];
};
|
Add a Member Function
Declaration
To identify a function as a member, we insert its prototype into the
definition of its compound type.
For example, to add display() as a
member of our Student type, we insert
its full prototype:
struct Student {
int no;
char grade[14];
void display() const; // member function
};
|
The const qualifier identifies
this member function as a query.
display() cannot change
the state of any Student object.
That is, it cannot change either no
or any character in grade.
As a member function, display() has
direct access to the variables no and
grade.
Definition
We define display() in the implementation
file as follows:
void Student::display() const {
cout << no << ' ' << grade;
}
|
This definition consists of four elements:
- the Student:: prefix on the
function name identifies it as a member of our
Student type
- the empty parameter list - this function does not receive
any values from or pass any values through the parameter list
to the client
- the const qualifier
identifies this function as a query -
it cannot change any of the values of the data members
in an instance of Student type
- the data members - the function can access no
and grade, which are defined outside
the function but within the definition of the compound type
Call a Member Function
The client calls a member function in the same way that it refers to
a data member of an instance of a compound type. The call consists
of the object's name, the . operator and the
member's name.
For example, if harry is an object
of Student type, we can display its data
by calling display() on harry
as follows:
Student harry = {975, {'A','B','C'}};
harry.display();
cout << endl;
|
The object part of the function call identifies the data that
the function accesses.
Member Functions are not Global Functions
Global functions differ from member functions of the same name.
A client calls a global function without referring to any object.
On the other hand, a client can only call a member function on an object
of some type. A client cannot call a member functions that is not
attached to an compound type.
Defining a global function with the same name as a member function does not
introduce a conflict, since calls to each involve different syntax.
display(); // calls the global display function
harry.display(); // calls the display member function on harry
|
Calling a Global Function from within a Member Function
Calling a global function from within member function of a compound type
that contains a member function with the same name as the global function
requires special attention. To call the global function we apply the
scope resolution operator in the call to the global function. To call
the member function, we simply call the function's identifier:
void Student::foo() {
::display(); // calls the global display function
display(); // calls the display member function
}
|
Privacy
Defining a compound type using the struct keyword
exposes the data members to the client. To limit accessibility, C++
lets us introduce a layer of privacy. To hide the information stored
in any member, we can classify that member as private.
The only members that a client needs to access are the compound type's
communication links. These links are the parameters and return values
of the type's member function. The client does not need to see the
data that describes an object's state.
Accessibility Labels
To prohibit external access to any member (data or function), we insert
the label private into the definition
of our compound type:
private:
private identifies all subsequent
members listed in the definition as inaccessible.
To allow client access, we insert the label
public:
public:
public identifies all subsequent members
listed in the definition as accessible.
For example, if we choose to
- hide the data members of each Student object
- expose the member function of the Student type
we define the Student type as
follows
struct Student {
private:
int no;
char grade[14];
public:
void display() const;
};
|
With this definition of our Student type, any attempt to
access a data member of a Student object generates
a complier error:
void foo(const Student& harry) {
cout << harry.no; // ERROR - member is private!
}
|
The function foo() can only access the
data stored in harry indirectly through
public member function display().
void foo(const Student& harry) {
harry.display();
}
|
Modifying Private Data
If the data members within a compound type are private,
the client cannot initialize their values directly.
in such cases, we need a separate member function that can
modify the data within an object.
For example, to store data in Student
objects, let us introduce a public member
function named set():
struct Student {
private:
int no;
char grade[14];
public:
void set(int n, const char* g);
void display() const;
};
|
set() is a modifier: its declaration
does not include the const keyword.
set() receives from the client a
student number and the address of a C-style string that contains
a set of grades and stores this information in the object's data
members:
void Student::set(int n, const char* g) {
int i;
no = n; // store the Student number as received
// store the grades as received within the available space
for (i = 0; g[i] != '\0' && i < 13; i++) {
grade[i] = g[i];
grade[i] = '\0'; // set the last byte to null
}
|
Communications Links
The set() and
display() member functions
are now the only communication links to a client.
Any client can call set() or display()
on any Student object, but no client
can access to the data stored within any
Student object directly.
For example,
Student harry;
harry.set(975, "ABC");
harry.display();
cout << harry.no; // ERROR .no IS PRIVATE!
|
Empty State
with hidding data members, we can control which data to accept
and which to reject from a client. That is, we can check
if the received values are valid before accepting them.
If the values are invalid, we can reject them and store values
that represent an empty state.
Upgrade set()
Let us upgrade our set() member function
to accept external data only if the student number is
positive-valued and the grades are A, B, C, D or F, without exception.
If the data received from the client fails to meet all of these conditions,
let us ignore that data and store values that represent an empty state:
void Student::set(int n, const char* g) {
int i;
bool valid = true; // assume valid input, check invalidity
if (n < 1)
valid = false;
else {
for (i = 0; g[i] != '\0' && valid; i++)
valid = g[i] >= 'A' && g[i] <= 'F' && g[i] != 'E';
if (valid) {
// accept client's data
no = n;
for (i = 0; g[i] != '\0' && i < 13; i++)
grade[i] = g[i];
grade[i] = '\0'; // set the last byte to the null byte
}
else {
// ignore client's data, set an empty state
no = 0;
grade[0] = '\0';
}
}
|
The validation logic added to our upgraded set() function
ensures that the data stored in a Student object is either
valid data or data that represents an empty state.
Upgrading display()
To complete this enhancement, let us upgrade our display()
member function to ensure that it executes gracefully if our object happens
to be in an empty state.
void Student::display() const {
if (no > 0)
cout << no << ' ' << grade;
else
cout << "no data available";
}
|
Completing the Upgrade
Note that this upgrade still leaves the data in a Student
object undefined before the first call to set().
To complete the upgrade and fill this hole, we will introduce a special member
function in the chapter entitled Classes.
Inlining
A member function that does not contain any iterations may be a candidate
for inlining, which is a technique that improves the execution time
for a function call at the cost of increasing the size of the executable
code. An inline request asks the compiler to insert the body of the
function at every call to the function. The compiler, instead of
storing the function once in its own dedicated region of memory and
inserting instructions to transfer control to that region from each call,
inserts a copy of the body at each and every call. Although inlining
creates a larger executable, it avoids the overhead of a function call.
Inlining is particularly useful with member functions that contain
small blocks of code.
The compiler determines whether or not to inline a function.
If the function has too many statements or contains an iteration, the
compiler ignore the request and call the function in the usual way.
To inline a member function, we either embed its definition within
the class definition or add the keyword inline
as a definition modifier.
For example,
// Inline Functions - Embedded
// inline_1.h
#include <cstring>
using namespace std;
class Student {
int no;
char grade[14];
public:
void set(int n, const char* g);
void display() const {
cout << no << ' ' << grade;
}
};
|
// Inline Functions - Separate
// inline_2.h
#include <cstring>
using namespace std;
class Student {
int no;
char grade[14];
public:
void set(int n, const char* g);
void display() const;
};
inline void display() const {
cout << no << ' ' << grade;
}
|
The implementation of any inline function belongs in the module's
header file alongside the class definition.
Summary
- a C++ compound type may contain both data members and member functions
- data members hold information about the state of an object
- member functions describe the logic that an object performs on its data members
- a query reports the state of an object without changing its state
- a modifier changes the state of an object
- the keyword private identifies
subsequent members as inaccessible to clients
- the keyword public identifies
subsequent members as
accessible to clients
- an empty state is the set of data values that represents an
absence of valid data in an object
- a compiler replaces each call statement to an inline function with
a copy of the body of the function
Exercises
|