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:

  • char
  • int
  • float
  • double

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

 int noSubjects;

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

 instance.member

To access harry's student number, we write

 harry.no

We retrieve the address of a non-array member using the address of operator (&)

 &instance.member

The address of harry's student number is given by

 &harry.no

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

 harry.grade

We access an element of an array member using subscript notation

 instance.member[index]

harry's third grade is given by

 harry.grade[2]

We retrieve the address of an element of an array member using the address of operator (&)

 &instance.member[index]

The address of harry's third grade is given by

 &harry.grade[2]
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

Previous Reading  Previous: Rudiments Next: Compound Types II   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo
Creative Commons License