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:
1 2 3 4 5 6 |
std::string myName{}; int myBirthYear{}; int myBirthMonth{}; int myBirthDay{}; int myHeightInches{}; int myWeightPounds{}; |
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:
1 2 3 4 5 6 |
struct Employee { int id{}; int age{}; double wage{}; }; |
This tells the compiler that we are defining a struct named Employee. The Employee struct contains 3 variables inside of it: an int 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:
1 |
Employee joe{}; // struct Employee is capitalized, variable joe is not |
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:
1 2 |
Employee joe{}; // create an Employee struct for Joe Employee frank{}; // create an Employee struct for Frank |
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:
1 2 3 4 5 6 7 8 9 |
Employee joe{}; // create an Employee struct for Joe joe.id = 14; // assign a value to member id within struct joe joe.age = 32; // assign a value to member age within struct joe joe.wage = 24.15; // assign a value to member wage within struct joe Employee frank{}; // create an Employee struct for Frank frank.id = 15; // assign a value to member id within struct frank frank.age = 28; // assign a value to member age within struct frank frank.wage = 18.27; // assign a value to member wage within struct frank |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int totalAge{ joe.age + frank.age }; if (joe.wage > frank.wage) std::cout << "Joe makes more than Frank\n"; else if (joe.wage < frank.wage) std::cout << "Joe makes less than Frank\n"; else std::cout << "Joe and Frank make the same amount\n"; // Frank got a promotion frank.wage += 2.50; // Today is Joe's birthday ++joe.age; // use pre-increment to increment Joe's age by 1 |
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.
1 2 3 4 5 6 7 8 9 |
struct Employee { int id{}; int age{}; double wage{}; }; Employee joe{ 1, 32, 60000.0 }; // joe.id = 1, joe.age = 32, joe.wage = 60000.0 Employee frank{ 2, 28 }; // frank.id = 2, frank.age = 28, frank.wage = 0.0 (default 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.
Non-static member initialization
It’s possible to give non-static (normal) struct members a default value:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct Rectangle { double length{ 1.0 }; double width{ 1.0 }; }; int main() { Rectangle x{}; // length = 1.0, width = 1.0 x.length = 2.0; // you can assign other values like normal return 0; } |
If both non-static member initializer and list-initialization are provided, the list-initialization takes precedence.
1 2 3 4 5 6 7 8 9 10 11 12 |
struct Rectangle { double length{ 1.0 }; double width{ 1.0 }; }; int main() { Rectangle x{ 2.0, 2.0 }; return 0; } |
In the above example, Rectangle x would be initialized with length and width 2.0.
We talk about what static members are in a later chapter. For now, don’t worry about them.
Assigning to structs
If we wanted to assign values to the members of structs, we can to do so individually:
1 2 3 4 5 6 7 8 9 10 11 |
struct Employee { int id{}; int age{}; double wage{}; }; Employee joe{}; joe.id = 1; joe.age = 32; joe.wage = 60000.0; |
If you want to assign a new value to all members, this is a pain, particularly for structs with many members. You can also assign values to structs members using an initializer list:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct Employee { int id{}; int age{}; double wage{}; }; Employee joe{}; joe = { 1, 32, 60000.0 }; // This is the same as joe = Employee{ 1, 32, 60000.0 }; // It's also possible to copy all members from one variable to another Employee emma{ joe }; |
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:
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 31 |
#include <iostream> struct Employee { int id{}; int age{}; double wage{}; }; void printInformation(Employee employee) { std::cout << "ID: " << employee.id << '\n'; std::cout << "Age: " << employee.age << '\n'; std::cout << "Wage: " << employee.wage << '\n'; } int main() { Employee joe { 14, 32, 24.15 }; Employee frank { 15, 28, 18.27 }; // Print Joe's information printInformation(joe); std::cout << '\n'; // Print Frank's information printInformation(frank); return 0; } |
In the above example, we pass an entire Employee struct to printInformation() (by value, meaning the argument is copied into the parameter). 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.
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 31 32 33 34 35 36 37 38 39 40 41 42 |
#include <iostream> struct Point3d { double x{}; double y{}; double z{}; }; Point3d getZeroPoint() { // We can create a variable and return the variable. Point3d temp { 0.0, 0.0, 0.0 }; return temp; } Point3d getZeroPoint2() { // We can return directly. We already specified the type // at the function declaration (Point3d), so we don't need // it again here. return { 0.0, 0.0, 0.0 }; } Point3d getZeroPoint3() { // We can use empty curly braces to zero-initialize all // members of `Point3d`. return {}; } int main() { Point3d zero{ getZeroPoint() }; if (zero.x == 0.0 && zero.y == 0.0 && zero.z == 0.0) std::cout << "The point is zero\n"; else std::cout << "The point is not zero\n"; return 0; } |
This prints:
The point is zero
Nested structs
Structs can contain other structs. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct Employee { int id{}; int age{}; double wage{}; }; struct Company { Employee CEO{}; // Employee is a struct within the Company struct int numberOfEmployees{}; }; Company myCompany; |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct Employee { int id; int age; double wage; }; struct Company { Employee CEO; // Employee is a struct within the Company struct int numberOfEmployees; }; Company myCompany{{ 1, 42, 60000.0 }, 5 }; |
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, but with fixed-size integers and `id` being half the size of `age`. On many platforms, 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <cstdint> #include <iostream> struct Employee { // We're using fixed-width integers for the sake of the example. // Avoid them in real code. std::int16_t id{}; std::int32_t age{}; double wage{}; }; int main() { std::cout << "The size of a double is " << sizeof(double) << '\n'; std::cout << "The size of Employee is " << sizeof(Employee) << '\n'; return 0; } |
On the author’s machine, this prints:
The size of a double is 8 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 [1] 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 in the declaration in the header and define the variable in a source file.
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 time
Question #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).
Show Solution [2]
Question #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 returns the result as a decimal number. You do not need to reduce the fraction to its lowest terms. Print the result of the multiplication of the 2 fraction variables.
Show Solution [2]
![]() |
![]() |
![]() |