C++ Structures
A structure (declared with the struct keyword) is a user-defined type that groups multiple related variables — possibly of different types — under a single name. Instead of tracking a person’s name, age, and salary as three separate loose variables, a structure lets you bundle them into one Person type and pass that single unit around your program. Structures are the foundation for organizing real-world data in C++ and are the direct ancestor of classes, so understanding them thoroughly also prepares you for object-oriented programming.
Overview / How Structures Work
A struct defines a new composite type made up of members (also called fields) — each with its own name and type. When you declare a variable of that struct type, the compiler reserves enough memory to hold all of its members, laid out in (roughly) the order they were declared. The compiler may insert extra padding bytes between members so each one starts at a memory address that matches its type’s alignment requirement — this is invisible in your code but affects sizeof results. You access a member using the dot operator (variable.member) on a struct variable, or the arrow operator (pointer->member) on a pointer to a struct.
In C++ (unlike C), struct is essentially identical to class. The only functional difference is the default access specifier: members and base classes of a struct are public by default, while a class defaults to private. Because of this, you can add member functions, constructors, destructors, and operator overloads to a struct exactly as you would to a class. By convention, C++ programmers reach for struct when a type is a simple, mostly-public bundle of data (sometimes called a POD, or Plain Old Data, type) and reach for class when a type needs to hide its internal state and enforce invariants — but the compiler does not force this distinction on you.
A struct variable can live on the stack (a local variable, destroyed automatically when it goes out of scope), as a global/static variable, or on the heap (allocated with new, and must be released with delete). Structs can be nested inside other structs, stored in arrays or containers like vector<Employee>, and passed to functions either by value (which copies every member) or by reference/pointer (which avoids the copy and lets the function modify the original).
Syntax
The general form of a struct definition is:
struct StructName {
type member1;
type member2;
// ... more members
returnType memberFunction(parameters) {
// optional member function
}
};
| Part | Meaning |
|---|---|
struct |
Keyword that begins a structure definition. |
StructName |
The tag/name of the new type. Use UpperCamelCase by convention. |
member1, member2 |
Data members (fields). Each has a type and a name, just like a regular variable declaration. |
memberFunction |
An optional function defined inside the struct that can read/modify its own members. |
; after } |
Required. The closing brace of a struct definition must be followed by a semicolon. |
To create and use a struct variable:
StructName variableName; // default-construct, members uninitialized (for built-in types)
StructName variableName{val1, val2}; // aggregate initialization, in member order
variableName.member1 = someValue; // access via the dot operator
StructName* ptr = &variableName;
ptr->member1 = someValue; // access via a pointer, using the arrow operator
Examples
Example 1: A basic struct
#include <iostream>
#include <string>
using namespace std;
struct Book {
string title;
string author;
int pages;
double price;
};
int main() {
Book myBook;
myBook.title = "The C++ Programming Language";
myBook.author = "Bjarne Stroustrup";
myBook.pages = 1376;
myBook.price = 59.99;
cout << "Title: " << myBook.title << endl;
cout << "Author: " << myBook.author << endl;
cout << "Pages: " << myBook.pages << endl;
cout << "Price: $" << myBook.price << endl;
return 0;
}
Output:
Title: The C++ Programming Language
Author: Bjarne Stroustrup
Pages: 1376
Price: $59.99
Here Book groups four different pieces of data into one type. We declare a single variable, myBook, and set each field individually using the dot operator. Without a struct, we would have needed four separate, disconnected variables that could easily get mismatched when passed around.
Example 2: An array of structs with a function
#include <iostream>
#include <string>
using namespace std;
struct Student {
string name;
int score;
};
double averageScore(Student students[], int count) {
int sum = 0;
for (int i = 0; i < count; i++) {
sum += students[i].score;
}
return static_cast<double>(sum) / count;
}
int main() {
Student students[3] = {
{"Alice", 92},
{"Bob", 85},
{"Charlie", 78}
};
for (int i = 0; i < 3; i++) {
cout << students[i].name << ": " << students[i].score << endl;
}
cout << "Average score: " << averageScore(students, 3) << endl;
return 0;
}
Output:
Alice: 92
Bob: 85
Charlie: 78
Average score: 85
This example shows a struct stored in an array, which is common when modeling a collection of similar records (students, employees, products). The averageScore function receives the array and its length, loops through each Student, and accumulates the score member. Because the array is passed as a pointer under the hood, no copying of the whole array occurs.
Example 3: Nested structs, member functions, and pointers
#include <iostream>
#include <string>
using namespace std;
struct Point {
double x;
double y;
};
struct Rectangle {
Point topLeft;
double width;
double height;
double area() const {
return width * height;
}
};
void printRectangle(const Rectangle& r) {
cout << "Top-left: (" << r.topLeft.x << ", " << r.topLeft.y << ")" << endl;
cout << "Width: " << r.width << ", Height: " << r.height << endl;
cout << "Area: " << r.area() << endl;
}
int main() {
Rectangle rect{ {1.0, 2.0}, 4.0, 3.0 };
printRectangle(rect);
Rectangle* rectPtr = ▭
rectPtr->width = 10.0;
cout << "Updated area: " << rectPtr->area() << endl;
return 0;
}
Output:
Top-left: (1, 2)
Width: 4, Height: 3
Area: 12
Updated area: 30
Rectangle nests a Point struct inside itself and defines a member function, area(), that reads its own width and height. The function printRectangle takes the struct by const reference so it can inspect it without copying. Then we take the address of rect with &, store it in a Rectangle*, and use the arrow operator to modify width through the pointer — showing that pointer-based access changes the original object.
Under the Hood
When the compiler sees a struct definition, it does not allocate any memory — a struct definition is just a blueprint, exactly like a class. Memory is only reserved when you declare a variable of that type. The size of a struct variable is at least the sum of its members’ sizes, but the compiler typically adds padding bytes so each member sits at an address divisible by its own alignment requirement (for example, an int usually needs to start at a 4-byte boundary). This is why sizeof(SomeStruct) can be larger than the sum of sizeof of its individual members — reordering members from largest to smallest can sometimes reduce this padding.
Copying a struct (through assignment, pass-by-value, or return-by-value) performs a memberwise copy by default: the compiler generates a copy constructor and assignment operator that copy each member in turn. For members that are themselves objects with their own copy semantics (like std::string), this means the whole sub-object is deep-copied correctly. Passing a large struct by value therefore has a real performance cost, which is why performance-sensitive code passes structs by const reference (as printRectangle does above) instead of by value.
Common Mistakes
Mistake 1: Forgetting the semicolon after the struct definition
Wrong code (this will not compile):
struct Point {
int x;
int y;
}
int main() {
Point p;
p.x = 5;
return 0;
}
The struct definition is missing a closing semicolon after }. The compiler treats the next line as part of the struct’s declaration and produces a confusing syntax error. The fix is simple — always close a struct definition with };:
struct Point {
int x;
int y;
};
int main() {
Point p;
p.x = 5;
return 0;
}
Mistake 2: Comparing structs with == before defining it
Wrong code (this will not compile, because the compiler does not generate operator== for you):
struct Point {
int x;
int y;
};
int main() {
Point a{1, 2};
Point b{1, 2};
if (a == b) {
cout << "Equal" << endl;
}
return 0;
}
Unlike some languages, C++ does not automatically compare structs member-by-member with ==. You must overload the operator yourself:
#include <iostream>
using namespace std;
struct Point {
int x;
int y;
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
int main() {
Point a{1, 2};
Point b{1, 2};
if (a == b) {
cout << "Equal" << endl;
} else {
cout << "Not equal" << endl;
}
return 0;
}
Output:
Equal
Mistake 3: Expecting pass-by-value to modify the caller’s struct
This code compiles fine but produces a surprising result, because increment receives a copy of the struct:
#include <iostream>
using namespace std;
struct Counter {
int value;
};
void increment(Counter c) {
c.value++;
}
int main() {
Counter counter{10};
increment(counter);
cout << "Value: " << counter.value << endl;
return 0;
}
Output:
Value: 10
The counter.value in main never changes because increment only modified its local copy. To modify the original, pass the struct by reference:
#include <iostream>
using namespace std;
struct Counter {
int value;
};
void increment(Counter& c) {
c.value++;
}
int main() {
Counter counter{10};
increment(counter);
cout << "Value: " << counter.value << endl;
return 0;
}
Output:
Value: 11
Best Practices
- Use
structfor simple, mostly-public data aggregates with little or no behavior; reach forclasswhen you need to hide implementation details and enforce invariants. - Name structs with UpperCamelCase (
Point,Book,Employee) to distinguish types from variables. - Initialize every member, either through aggregate initialization (
Point p{1, 2};) or default member initializers (int x = 0;), so you never read garbage values. - Pass large structs by
constreference to functions that only read them, to avoid an unnecessary copy. - Overload operators such as
==and<<when your struct needs to be compared or printed, since the compiler does not generate these for you. - Group related fields logically, and nest structs (like
RectanglecontainingPoint) instead of flattening everything into one giant struct. - Be aware of padding and alignment if the exact memory layout matters (for example, when serializing data or talking to hardware); ordering members from largest to smallest can reduce wasted space.
Practice Exercises
- Define a struct
Carwith membersbrand(string),model(string),year(int), andprice(double). Create twoCarvariables with different data and print all four fields for each. - Define a struct
Temperaturewith adouble celsiusmember and a member functiontoFahrenheit()that returnscelsius * 9.0 / 5.0 + 32.0. Test it with the values 0, 100, and 37. - Define an array of 5
Employeestructs (each with astring nameand adouble salary), and write a function that scans the array and returns theEmployeewith the highest salary.
Summary
- A
structgroups related variables of possibly different types into one named type. - Members are accessed with the dot operator (
.) on a variable and the arrow operator (->) on a pointer. - In C++,
structbehaves likeclassexcept that its members default topublic. - Structs can contain member functions, be nested inside each other, stored in arrays, and passed by value or by reference.
- Passing a struct by value copies every member; pass by reference when you want a function to see or modify the original.
- The compiler does not generate
operator==or a print operator for you — overload them yourself when needed. - Compilers may insert padding between members for alignment, so
sizeof(Struct)can exceed the sum of its members’ sizes.
