Consider an employee represented by 3 loose variables:
1 2 3 4 5 6 7 8 |
int main() { short id = 1; int age = 24; double wage = 52400.0; return 0; } |
If we want to pass this employee to a function, we have to pass three variables:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> void printEmployee(short id, int age, double wage) { std::cout << "ID: " << employee.id << '\n'; std::cout << "Age: " << employee.age << '\n'; std::cout << "Wage: " << employee.wage << '\n'; } int main() { short id = 1; int age = 24; double wage = 52400.0; printEmployee(id, age, wage); return 0; } |
While passing 3 variables isn’t that bad, consider a struct with 10 or 12 members. Passing each variable independently would be time consuming and error prone. Additionally, if we ever add a new attribute to our employee (e.g. name), we now have to modify all the functions and function calls to accept the new parameter & argument!
Fortunately, we can pass entire structs to functions.
Passing structs (by reference)
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 { short id; int age; double wage; }; void printEmployee(const 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 printEmployee(joe); std::cout << '\n'; // Print Frank's information printEmployee(frank); return 0; } |
In the above example, we pass an entire Employee struct to printInformation() (twice, once for joe
and once for frank
). Note that because structs are class types, we generally pass them by const reference to avoid making an unnecessary copy!
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
Returning structs
Consider the case where we have a function that needs to return a point in 3-dimensional Cartesian space. Such a point has 3 attributes: an x coordinate, a y coordinate, and a z coordinate. But functions can only return one value. So how do we return all 3 coordinates back the user?
One common way is to return a struct.
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 |
#include <iostream> struct Point3d { double x { 0.0 }; double y { 0.0 }; double z { 0.0 }; }; Point3d getZeroPoint() { // We can create a variable and return the variable (we'll improve this below) Point3d temp { 0.0, 0.0, 0.0 }; return temp; } 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
Note that we’ll generally return structs by value, so as not to return a dangling reference.
Returning temporary structs
In the getZeroPoint
function above, we create a new named object (temp
) to do nothing but return it. We can improve this by returning a temporary (unnamed) object instead:
1 2 3 4 |
Point3d getZeroPoint() { return Point3d { 0.0, 0.0, 0.0 }; // return a temporary Point3d } |
Note how much cleaner this is (one line vs two, and no need to understand whether temp
is used more than once).
In the case where the function returns an explicit type (e.g. Point3d
) rather than auto
, we can omit the type in the return statement:
1 2 3 4 5 6 |
Point3d getZeroPoint() { // We already specified the type at the function declaration // so we don't need to do so here again return { 0.0, 0.0, 0.0 }; // return a temporary Point3d } |
Also note that since in this case we’re returning all zero values, we can use empty braces to zero-initialize all the member:
1 2 3 4 5 |
Point3d getZeroPoint3() { // We can use empty curly braces to zero-initialize all members return {}; } |
Nested structs
Structs can contain other structs. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct Employee { short id; int age; double wage; }; struct Company { int numberOfEmployees; Employee CEO; // Employee is a struct within the Company struct }; int main() { Company myCompany{ 7, { 1, 32, 55000.0 } }; } |
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
.
Note that our list-initialization contains a nested list for the nested struct.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct Employee { short id; int age; double wage; }; int main() { std::cout << "The size of Employee is " << sizeof(Employee) << "\n"; return 0; } |
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.
For advanced readers
The reason compilers may add padding 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
As a program-defined type, any file that wants to use a struct type needs to be able to access the definition of the struct. For this reason, struct definitions are typically put in header files, and #included anywhere they are needed.
Struct variables behave like normal variables, so they can be given external linkage and accessed across multiple files just like any normal variable can.
Structs are an important building block
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.
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).
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 prints the result out as a decimal number. You do not need to reduce the fraction to its lowest terms.
![]() |
![]() |
![]() |
Leave a Reply