Structs in C++

There are many instances in programming where we need more than one variable in order to represent something. For example, to represent yourself, you might want to store your name, your birthday, your height, your weight, or any other number of characteristics about yourself. You could do so like this:
1
2
3
4
5
6
char strName[20];
int nBirthYear;
int nBirthMonth;
int nBirthDay;
int nHeight; // in inches
int nWeight; // in pounds
However, you now have 6 independent variables that are not grouped in any way. If you wanted to pass information about yourself to a function, you’d have to pass each variable individually. Furthermore, if you wanted to store information about more people, you’d have to declare 6 more variables for each additional person! As you can see, this can quickly get out of control.
Fortunately, C++ allows us to create our own user-defined aggregate data types. An aggregate data type is a data type that groups multiple individual variables together. One of the simplest aggregate data type is the struct. A struct (short for structure) allows us to group variables of mixed data types together into a single unit.

Because structs are user-defined, we first have to tell the compiler what our struct looks like before we can begin using it. To do this, we declare our struct using the struct keyword. Here is an example of a struct declaration:
1
2
3
4
5
6
struct Employee
{
    int nID;
    int nAge;
    float fWage;
};
This tells the compiler that we are defining a struct named Employee. The Employee struct contains 3 variables inside of it: two ints and a float. These variables are called members (or fields). Keep in mind that the above is just a declaration — even though we are telling the compiler that the struct will have variables, no memory is allocated at this time.
In order to use the Employee struct, we simply declare a variable of type Employee:
1
Employee sJoe;
sJoe is a variable of type Employee. As with normal variables, declaring a variable allocates memory for that variable. Typically, the size of a struct is the sum of the size of all it’s members. In this case, since each integer is 4 bytes and a float is 4 bytes, Employee would be 12 bytes. However, some platforms have specific rules about how variables must be laid out in memory — consequently, the compiler may leave gaps between the variables. As a result, we can say the struct will be at minimum 12 bytes.
To find out the exact size of Employee, we can use the sizeof operator:
1
cout << "The size of Employee is " << sizeof(Employee);
On the author’s Pentium 4, this prints The size of Employee is 12.
When we declare a variable such as Employee sJoe, sJoe refers to the entire struct (which contains the member variables). In order to access the individual members, we use the member selection operator (which is a period). As with normal variables, struct member variables are not initialized, and will typically contain junk. We must initialize them manually. Here is an example of using the member selection operator to initialize each member variable:
1
2
3
4
Employee sJoe;
sJoe.nID= 14; // initialize nID within sJoe
sJoe.nAge = 32; // initialize nAge within sJoe
sJoe.fWage = 24.15; // initialize fWage within sJoe
It is possible to declare multiple variables of the same struct type:
1
2
3
4
5
6
7
8
9
Employee sJoe; // create an Employee struct for Joe
sJoe.nID = 14;
sJoe.nAge = 32;
sJoe.fWage = 24.15;
Employee sFrank; // create an Employee struct for Frank
sFrank.nID = 15;
sFrank.nAge = 28;
sFrank.fWage = 18.27;
In the above example, it is very easy to tell which member variables belong to Joe and which belong to Frank. This provides a much higher level of organization than individual variables would. Furthermore, because the members all have the same name, this provides consistency across multiple variables of the same type.
Struct member variables act just like normal variables, so it is possible to do normal operations on them:
1
2
3
4
5
6
7
8
9
10
int nTotalAge = sJoe.nAge + sFrank.nAge;
if (sJoe.fWage > sFrank.fWage)
    cout << "Joe makes more than Frank" << endl;
// Frank got a promotion
sFrank.fWage += 2.50;
// Today is Joe's birthday
sJoe.nAge++;
Another big advantage of using structs over individual variables is that we can pass the entire struct to a function that needs to work with the members:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
void PrintInformation(Employee sEmployee)
{
    using namespace std;
    cout << "ID:   " << sEmployee.nID << endl;
    cout << "Age:  " << sEmployee.nAge << endl;
    cout << "Wage: " << sEmployee.fWage << endl << endl;
}
int main()
{
    Employee sJoe; // create an Employee struct for Joe
    sJoe.nID = 14;
    sJoe.nAge = 32;
    sJoe.fWage = 24.15;
    Employee sFrank; // create an Employee struct for Frank
    sFrank.nID = 15;
    sFrank.nAge = 28;
    sFrank.fWage = 18.27;
    // Print Joe's information
    PrintInformation(sJoe);
    // Print Frank's information
    PrintInformation(sFrank);
    return 0;
}
In the above example, we pass an entire Employee struct to PrintInformation(). This prevents us from having to pass each variable individually. Furthermore, if we ever decide to add new members to our Employee struct, we will not have to change the function declaration or function call!
PrintInformation() uses the Employee struct passed to it to print out employee information to the screen. The above program outputs:
ID:   14
Age:  32
Wage: 24.15

ID:   15
Age:  28
Wage: 18.27
Structs can contain other structs. For example:
1
2
3
4
5
6
7
struct Company
{
    Employee sCEO; // Employee is a struct within the Company struct
    int nNumberOfEmployees;
};
Company sMyCompany;
In this case, if we wanted to know what the CEO’s salary was, we simply use the member selection operator twice: sMyCompany.sCEO.fWage;
This selects the sCEO member from sMyCompany, and then selects the fWage member from within sCEO.
Initializer lists
Initializing structs member by member is a little cumbersome, so C++ supports a faster way to initialize structs using an initializer list. This allows you to initialize some or all the members of a struct at declaration time.
1
2
3
4
5
6
7
8
struct Employee
{
    int nID;
    int nAge;
    float fWage;
};
Employee sJoe = {1, 42, 60000.0f}; // nID=1, nAge=42, fWage=60000.0
You can use nested initializer lists for nested structs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Employee
{
    int nID;
    int nAge;
    float fWage;
};
struct Company
{
    Employee sCEO; // Employee is a struct within the Company struct
    int nNumberOfEmployees;
};
Company sCo1 = {{1, 42, 60000.0f}, 5};
A few final notes on structs
The “m_” Hungarian Notation prefix for members is typically not used for structs, even though structs contain members. This is (in part) because all variables in a struct are members! Consequently, labeling them with a “m_” prefix is somewhat redundant.
It is common to declare structs in a header file, so they can be accessed by multiple source files.
The class aggregate data type builds on top of the functionality offered by structs. Classes are at the heart of C++ object-oriented programming. Understanding structs is the first step towards object-oriented programming!
Previous
Next Post »