Search

4.7 — Structs

There are many instances in programming where we need more than one variable in order to represent an object. 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:

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 someone else, 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 types is the struct. A struct (short for structure) allows us to group variables of mixed data types together into a single unit.

Declaring and defining structs

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:

This tells the compiler that we are defining a struct named Employee. The Employee struct contains 3 variables inside of it: a short named id, an int named age, and a double named wage. These variables that are part of the struct are called members (or fields). Keep in mind that Employee is just a declaration -- even though we are telling the compiler that the struct will have member variables, no memory is allocated at this time. By convention, struct names start with a capital letter to distinguish them from variable names.

Warning: One of the easiest mistakes to make in C++ is to forget the semicolon at the end of a struct declaration. This will cause a compiler error on the next line of code. Modern compilers like Visual Studio 2010 will give you an indication that you may have forgotten a semicolon, but older or less sophisticated compilers may not, which can make the actual error hard to find.

In order to use the Employee struct, we simply declare a variable of type Employee:

This defines a variable of type Employee named joe. As with normal variables, defining a struct variable allocates memory for that variable.

It is possible to define multiple variables of the same struct type:

Accessing struct members

When we define a variable such as Employee joe, joe 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). Here is an example of using the member selection operator to initialize each member variable:

As with normal variables, struct member variables are not initialized, and will typically contain junk. We must initialize them manually.

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 Joe’s and Frank’s members have the same names, this provides consistency across multiple variables of the same struct type.

Struct member variables act just like normal variables, so it is possible to do normal operations on them:

Initializing structs

Initializing structs by assigning values 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.

In C++11, we can also use uniform initialization:

If the initializer list does not contain an initializer for some elements, those elements are initialized to a default value (that generally corresponds to the zero state for that type). In the above example, we see that frank.wage gets default initialized to 0.0 because we did not specify an explicit initialization value for it.

C++11/14: Non-static member initialization

Starting with C++11, it’s possible to give non-static (normal) struct members a default value:

Unfortunately, in C++11, the non-static member initialization syntax is incompatible with the initializer list and uniform initialization syntax. For example, in C++11, the following program won’t compile:

Consequently, in C++11, you’ll have to decide whether you want to use non-static member initialization or uniform initialization. Uniform initialization is more flexible, so we recommend sticking with that one.

However, in C++14, this restriction was lifted and both can be used. If both are provided, the initializer list/uniform initialization syntax takes precedence. In the above example, Rectangle x would be initialized with length and width 2.0. In C++14, using both should be preferred, as it allows you to declare a struct with or without initialization parameters and ensure the members are initialized.

We talk about what static members are in chapter 8. For now, don’t worry about them.

Assigning to structs

Prior to C++11, if we wanted to assign values to the members of structs, we had to do so individually:

This is a pain, particularly for structs with many members. In C++11, you can now assign values to structs members using an initializer list:

Structs and functions

A 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:

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!

The above program outputs:

ID:   14
Age:  32
Wage: 24.15

ID:   15
Age:  28
Wage: 18.27

A function can also return a struct, which is one of the few ways to have a function return multiple variables.

This prints:

The point is zero

Nested structs

Structs can contain other structs. For example:

In this case, if we wanted to know what the CEO’s salary was, we simply use the member selection operator twice: myCompany.CEO.wage;

This selects the CEO member from myCompany, and then selects the wage member from within CEO.

You can use nested initializer lists for nested structs:

Struct size and data structure alignment

Typically, the size of a struct is the sum of the size of all its members, but not always!

Consider the Employee struct. On many platforms, a short is 2 bytes, an int is 4 bytes, and a double is 8 bytes, so we’d expect Employee to be 2 + 4 + 8 = 14 bytes. To find out the exact size of Employee, we can use the sizeof operator:

On the author’s machine, this prints:

The size of Employee is 16

It turns out, we can only say that the size of a struct will be at least as large as the size of all the variables it contains. But it could be larger! For performance reasons, the compiler will sometimes add gaps into structures (this is called padding).

In the Employee struct above, the compiler is invisibly adding 2 bytes of padding after member id, making the size of the structure 16 bytes instead of 14. The reason it does this is beyond the scope of this tutorial, but readers who want to learn more can read about data structure alignment on Wikipedia. This is optional reading and not required to understand structures or C++!

Accessing structs across multiple files

Because struct declarations do not take any memory, if you want to share a struct declaration across multiple files (so you can instantiate variables of that struct type in multiple files), put the struct declaration in a header file, and #include that header file anywhere you need it.

Struct variables are subject to the same rules as normal variables. Consequently, to make a struct variable accessible across multiple files, you can use the extern keyword to do so.

Final notes on structs

Structs are very important in C++, as understanding structs is the first major step towards object-oriented programming! Later on in these tutorials, you’ll learn about another aggregate data type called a class, which is built on top of structs. Understanding structs well will help make the transition to classes that much easier.

The structs introduced in this lesson are sometimes called plain old data structs (or POD structs) since the members are all data (variable) members. In the future (when we discuss classes) we’ll talk about other kinds of members.

Quiz

1) You are running a website, and you are trying to keep track of how much money you make per day from advertising. Declare an advertising struct that keeps track of how many ads you’ve shown to readers, what percentage of ads were clicked on by users, and how much you earned on average from each ad that was clicked. Read in values for each of these fields from the user. Pass the advertising struct to a function that prints each of the values, and then calculates how much you made for that day (multiply all 3 fields together).

2) Create a struct to hold a fraction. The struct should have an integer numerator and an integer denominator member. Declare 2 fraction variables and read them in from the user. Write a function called multiply that takes both fractions, multiplies them together, and prints the result out as a decimal number. You do not need to reduce the fraction to its lowest terms.

Quiz Answers

1) Show Solution

2) Show Solution

4.8 -- The auto keyword
Index
4.6 -- Typedefs and type aliases

299 comments to 4.7 — Structs

  • derbutzemann

    I have tried something different to be sure about the new things before I go to next chapter.

    • nascardriver

      Hi derbutzemann!

      Use double numbers for doubles (0.0 instead of 0). I wouldn't have used the using directives for @std::cout and @std::cin but I guess it's controlled enough to be tolerated.
      You're ready for chapter 5.

  • DecSco

    Hey, if you find the time, do you have suggestions for improvement for the following code (Question 2)?

    • nascardriver

      Hi DecSco!

      @long is rarely used, at least I haven't seen it a lot, have a look at @<cstdint>, the data types in there have a guaranteed size.

      Initialize @num and @den in @getFractionFromUser, it won't change anything if you're using C++11 or later, but you shouldn't trust any function with uninitialized variables.

      References
      * <cstdint>: http://www.cplusplus.com/reference/cstdint/

      • DecSco

        Thank you! So you suggest to use, say, int32_t instead of long?
        And if I did a validity check on the user input, can I then skip initialisation (because I'd assign a value anyway if the user input is invalid) or what is the precise reason for not trusting a function with uninitialised variables?

        • nascardriver

          I thought you use @long the have a larger data type. If you don't want anything special go with @int. If you want a larger type use @std::int64_t.

          > can I then skip initialisation
          You could, but I'd still initialize the variable. A program should be deterministic, you should know it's state at any given point, with uninitialized variables this isn't the case.

          > what is the precise reason for not trusting a function with uninitialised variables?
          You don't know how the function works internally. You don't know if it uses the value for anything before overriding it. Even if you do, not every reader of your code knows which functions can be passed undefined values and which cannot.
          Uninitialized variables rarely cause trouble, but if they do, it's big trouble.

  • _RryanT

    I'm feeling like a bad programer

    My attempt at Quiz 1:

    Quiz 2:

    Thank you for those tutorials, I'm learning a lot. I just need more praticing xD

    • nascardriver

      Hi Ryan!

      Suggestions:
      * Initialize your variables with uniform initialization.
      * Quiz 1, Line 16: You're dividing int by int, this causes you to lose precision.
      * Quiz 2: Unnecessary forward declaration of @multiply. Move @multiply above @main.
      * Inconsistent use of '\n' and @std::endl. Pick one and stick to it unless you have a reason to mix them.

  • Serial Seth

    Anything I could do better?
    My attempt at question 1:

    • Serial Seth

      Lol, just saw the solution. Missed the part about user inputs to populate the struct... oh well.

    • nascardriver

      Hi Seth!

      Ignoring that you've missing the user input part, here are some suggestions:

      * You're using three different kinds of initialization, stick with uniform initialization.
      * Your @multiply function is pretty pointless, do the calculation in-line.
      * You're supposed to pass @march to a function which does the printing and calculations.
      * Which compiler are you using? '0()' isn't valid C++ and should cause an error.

      • Serial Seth

        Visual Studio Community compiled it. I don't know why I didn't get an error, I usually don't do the () on return values. I see the print and calculations function as an easy fix, so I won't post the update. I would like to know what you mean by 3 different types of initialization.

        Thank you for your input; been doing this only for a week. I will be posting more of my code in further lessons to make sure I am not developing bad habits. Again, it is nice to find a community here willing to help me in the next stage of my career.

        I plan on developing SCADA clients in the future with c,c++ controls and storage and java HMI. If anyone has any advice on subjects to make sure I master, please let me know.

        • nascardriver

          References
          Lesson 2.1 - Fundamental variable definition, initialization, and assignment

  • Samira Ferdi

    Can I input something (by using std::cin) to struct?

    • nascardriver

      Hi Samira!

      You'd need to either fill it manually

      or define an operator

  • Tony

    Hey Alex, since you said we'd better avoid the "casts", I've done my program like this (using double instead of ints, and just (numerator /  denominator) * (numerator1 / denominator2).
    It gives the total in decimal but I'm not sure if this is good practice?

    • Alex

      It's not good practice. When you're creating structs (or classes) the members should use the data type that best represents the thing you're modelling. For a fraction, the numerator and denominator should be integer values, and thus they should use int.

      Also, it's totally fine to use casts! Just use the C++ style casts (e.g. static_cast), not the C-style ones.

Leave a Comment

Put all code inside code tags: [code]your code here[/code]