Part B - Foundations
Compound Types I
Introduce and describe the syntax of a
structure
Describe how to access the members of an instance of a structure
Describe how to pass an instance of a structure to a function
"C++ has a set of fundamental types
corresponding to the most common basic storage
units of a computer and the most common ways of
using them to hold data. ... From these types,
we can construct other types" (Stroustrup, 1997)
Data Types
| Member Access
| Pass by Instance
| Pass by Value
| Pass By Address
Arrow Notation
| In-Class Exercise
| Summary
We can create data types from other data types. This language
facility lets us create our own data structures or chunks.
In this
chapter, we introduce the syntax for defining structures,
creating instances of them, and accessing the data values within those
instances.
Data Types
Data types in C and C++ are either primitive types or
compound types. Compound types are types
that are constructed from any combination of other compound
data types and primitive types.
Compound types types are also called user-defined types.
Primitive Types
The primitive types are part of the core language.
We cannot redefine them or introduce new primitive
types. Each primitive type describes how to interpret
information in a region of memory.
The primitive types include:
as well as pointers to them:
- char*
- int*
- float*
- double*
Consider the
int type. It stores
integral data in equivalent binary representation in 4 bytes
on a 32-bit platform:
int (32-bit platform)
|
1 Byte
|
1 Byte
|
1 Byte
|
1 Byte
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
To define an int
variable in memory called noSubjects,
we write
Structures
The definition of a structure takes the form
struct Tag {
//... declarations here
};
where the keyword struct identifies
a structure. Tag is the name by which we identify the structure.
The definition concludes with a semi-colon.
The definition contains uniquely named
data members. We list these members within the curly
braces:
struct Tag {
type identifier;
//... other members
};
where type is the type of the
member and identifier is the name
by which we call that member.
To define a structure called Student that holds a student's number and
his/her grades, we write
struct Student {
int no; // student number
float grade[4]; // grades
};
|
The two members occupy memory in the order of their
listing in the structure's definition:
struct |
Student |
member |
int
no |
char
grade[] |
bytes |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
That is, the definition describes the structure of
memory for a Student object
and the rules for interpreting the data stored within
that memory. Note that the definition itself does not allocate
any memory; it only describes the rules.
We define the structure globally and place its
definition in a header file named Student.h:
// Student.h
struct Student {
int no; // student number
char grade[14]; // grades
};
|
Allocating Memory
We allocate memory for an instance of a structure
by defining the instance. The definition
takes the form
struct Tag identifier;
where Tag is the name of the
structure and identifier is the
name of the instance.
To allocate memory for a Student named harry, we
write:
// main.cpp
#include "Student.h"
int main() {
struct Student harry;
// ...
}
|
struct |
Student
harry |
address |
2ff2b8c4 |
member |
int
no |
char
grade[] |
address |
2ff2b8c4 |
2ff2b8c8 |
bytes |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The name harry refers
to all of the data members in harry
taken together as an integral unit.
Initialization
We initialize an instance of a structure using a
braces-enclosed, comma-separated list of values
in the same order as the member listing
in the structure's definition. The
initialization takes the form
struct Tag identifier = { value, ... , value };
|
Note the similarity to array initialization.
To initialize harry
with student number 975 and grades of
'A', 'B' and
'C', we write
struct Student harry = { 975, {'A','B','C'}};
|
or more simply
struct Student harry = { 975, "ABC"};
|
struct |
Student
harry |
address |
2ff2b8c4 |
member |
int
no |
char
grade[] |
address |
2ff2b8c4 |
2ff2b8c8 |
value |
975 |
'A' |
'B' |
'C' |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
Member Access
We access a member of an instance of a structure using dot notation, which takes the form
To access harry's
student number, we write
We retrieve the address of a non-array member using the
address of operator (&)
The address of harry's student
number is given by
Note that parentheses - &(harry.no)
- are unnecessary because the dot operator binds tighter than the
address of operator in the precedence table.
We access an array member using the array
name without brackets. The address of harry's grades is given by
We access an element of an array member using subscript
notation
harry's third grade is given by
We retrieve the address of an element of an array member
using the address of operator (&)
The address of harry's third grade
is given by
struct |
Student
harry |
address |
&harry |
member |
int
no |
char
grade[] |
address |
&harry.no |
harry.grade |
value |
975 |
'A' |
'B' |
'C' |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
Pass An Instance
Member by Member
We can pass the data values stored in an instance
of a structure to a function member by member.
Consider the following program in which we pass
harry's student number and grades separately
to display():
// Passing data members to a function
// passingIndividualMembers.cpp
#include <iostream>
using namespace std;
struct Student {
int no;
char grade[14];
};
void display(int, const char*); // pass each member separately
int main( ) {
struct Student harry = {975, "ABC"};
display(harry.no, harry.grade);
}
void display(int no, const char* grade){
cout << "Grades for " << no;
cout << " : " << grade << endl;
}
|
Pass by Instance
Since harry refers to the memory occupied by
the instance itself, we may pass harry
as a single argument to display() instead:
// Passing data members to a function
// passingAnObject.cpp
#include <iostream>
using namespace std;
struct Student {
int no;
char grade[14];
};
void display(struct Student student); // pass the whole object
int main( ) {
struct Student harry = {975, "ABC"};
display(harry);
}
void display(struct Student student){
cout << "Grades for " << student.no;
cout << " : " << student.grade << endl;
}
|
Note that the definition of Student precedes the reference to Student in the display()
prototype. The compiler needs this definition to
interpret the parameter type in the function prototype.
Pass By Value
C and C++ compilers pass instances of compound types to
functions by value. They copy the value of each
argument in the function call into its corresponding function
parameter, as its initial value. Any change within the
function affects only this copy and does not alter
the original instance.
In the following example, the data stored in
harry does not change after
control reverts from set()
// Passing to a function
// passByValue.cpp
#include <iostream>
using namespace std;
struct Student {
int no;
char grade[14];
};
void set(struct Student student);
void display(struct Student student);
int main( ) {
struct Student harry = {975, "ABC"};
set(harry);
display(harry);
}
void set(struct Student student) {
struct Student jim = {306, "BBB"};
student = jim;
}
void display(struct Student student) {
cout << "Grades for " << student.no;
cout << " : " << student.grade << endl;
}
|
Grades for 975 : ABC
|
main() |
set() |
Student
harry |
Student
student |
Student
jim |
975 'A' 'B' 'C' |
975 'A' 'B' 'C' |
|
975 'A' 'B' 'C' |
975 'A' 'B' 'C' |
306 'B' 'B' 'B' |
975 'A' 'B' 'C' |
306 'B' 'B' 'B' |
306 'B' 'B' 'B' |
Copying
Note how in the above example we copied jim into student using
a single assignment expression. We do not need to copy each data
member separately. Copying an instance of a compound
type to another instance of
the same type is that simple.
Most compilers perform member-by-member
copying automatically in each of the following cases:
- pass an instance by value
- assign an instance to an existing instance
- initialize a new instance using an existing instance
- return an instance by value
Pass By Address
To change the data within an instance of a compound
type from within a function, we pass the address of that instance as an
argument in the function call. The function
receives the address in a pointer parameter.
In the following example, instead of passing a copy of
harry to set(), we pass the address of harry
// Passing the address of a struct
// passingAddress.cpp
#include <iostream>
#include <cstring>
using namespace std;
struct Student {
int no;
char grade[14];
};
void set(struct Student* student);
void display(struct Student student);
int main( ) {
struct Student harry = {975, "ABC"};
set(&harry);
display(harry);
}
void set(struct Student* student){
struct Student jim = {306, "BBB"};
*student = jim;
}
void display(struct Student student){
cout << "Grades for " << student.no;
cout << " : " << student.grade << endl;
}
|
Grades for 306 : BBB
|
int
main |
void
set |
void
display |
Student
harry |
Student*
student |
Student
jim |
Student
student |
2ff2b8d4 |
2ff2b8e6 |
2ff2b8ea |
2ff2b8e6 |
975 "ABC" |
|
|
|
306 "BBB" |
2ff2b8d4 |
306 "BBB" |
|
306 "BBB" |
|
|
306 "BBB" |
Efficiency
An efficient way of passing an instance to a
function is by address. Passing by address
avoids copying all of the data values, which saves
time and space particularily in cases
where one of the members of the compound type
is an array with many elements. Passing an instance by
address only copies and stores its address,
which typically occupies 4 bytes:
// Passing the address of a struct
// passingByAddress.cpp
#include <iostream>
#include <cstring>
using namespace std;
struct Student {
int no;
char grade[14];
};
void set(struct Student* student);
void display(struct Student* student);
int main( ) {
struct Student harry = {975, "ABC"};
set(&harry);
display(&harry);
}
void set(struct Student* student){
struct Student jim = {306, "BBB"};
*student = jim;
}
void display(struct Student* student){
cout << "Grades for " << (*student).no;
cout << " : " << (*student).grade << endl;
}
|
Grades for 306 : BBB
|
The syntax within display()
dereferences the address received before selecting the members
on that instance using the dot
operator. Since the dot operator binds tighter than the dereferencing
operator, the parentheses are necessary here. Omitting the
parentheses will cause a compiler error (the data member after the dot
operator is not of type Student*).
If we pass the address of an object to a function, but have no intention
to change that object within the function, we add the
const qualifier to safeguard against
modifications from within the function:
// Passing the address of a struct
// passingByAddressConst.cpp
#include <iostream>
#include <cstring>
using namespace std;
struct Student {
int no;
char grade[14];
};
void set(struct Student* student);
void display(const struct Student* student);
int main( ) {
struct Student harry = {975, "ABC"};
set(&harry);
display(&harry);
}
void set(struct Student* student){
struct Student jim = {306, "BBB"};
*student = jim;
}
void display(const struct Student* student){
cout << "Grades for " << (*student).no;
cout << " : " << (*student).grade << endl;
}
|
Grades for 306 : BBB
|
int
main |
void
set |
void
display |
Student
harry |
Student*
student |
Student
jim |
const
Student*
student |
2ff2b8d4 |
2ff2b8e6 |
2ff2b8ea |
2ff2b8e6 |
975 "ABC" |
|
|
|
306 "BBB" |
2ff2b8d4 |
306 "BBB" |
|
306 "BBB" |
|
|
2ff2b8d4 |
Arrow Notation
The syntax (*student).no
is awkward. Arrow notation provides
cleaner alternative:
address->member
The arrow operator takes a pointer to an object on its left side
and a member name on its right side.
For example,
// Passing the address of a struct
// arrowNotation.cpp
#include <iostream>
#include <cstring>
using namespace std;
struct Student {
int no;
char grade[14];
};
void set(struct Student* student);
void display(const struct Student* student);
int main( ) {
struct Student harry = {975, "ABC"};
set(&harry);
display(&harry);
}
void set(struct Student* student){
struct Student jim = {306, "BBB"};
*student = jim;
}
void display(const struct Student* student){
cout << "Grades for " << student->no;
cout << " : " << student->grade << endl;
}
|
Grades for 306 : BBB
|
In-Class Exercise
Rewrite the accounting program using a compound type:
- define a struct that describes
how the data for a single transaction is stored
- define and use an instance of this structure in the
main() function of the original
code
Complete the Handout on Compound
Data Types to implement this upgrade.
Summary
- the data members of a compound type may be primitive types or other compound types
- the members of an instance of a compound type occupy memory in the order listed in the declaration of the derived type
- the name of an instance of a derived type refers to the data in that instance as a whole
- the dot operator provides access to the selected member of an instance of a compound type
- passing an instance of a compound type by address is more efficient than passing it by value
- the arrow operator takes a pointer to an instance on its left side and a member name on its right side
|