Basic types such as int, float and char only describe single
values. In the real world, though, an entity usually consists of several attributes together. A
student, for instance, has a name (a string), an age (an integer) and a grade (a floating point
number). To group related pieces of information into one compound, user-defined type, C gives us the
struct keyword.
1. What is a struct?
A struct lets you group several variables of different types into a single new type. The component variables inside it are called the struct's members.
For example, defining a structure that represents a student:
#include <stdio.h>
#include <string.h>
// Dinh nghia struct Student
struct Student {
char name[50];
int age;
double score;
};
int main() {
// Khai bao va khoi tao bien kieu struct Student
struct Student sv1;
// Gan gia tri cho cac thanh vien bang toan tu cham (.)
strcpy(sv1.name, "Nguyen Van A");
sv1.age = 20;
sv1.score = 8.5;
// Khoi tao nhanh luc khai bao
struct Student sv2 = {"Tran Thi B", 19, 9.2};
// Truy xuat va in du lieu
printf("Sinh vien 1: %s, %d tuoi, %.1f diem\n", sv1.name, sv1.age, sv1.score);
printf("Sinh vien 2: %s, %d tuoi, %.1f diem\n", sv2.name, sv2.age, sv2.score);
return 0;
}
Note the semicolon after the closing brace of the struct β this is one of the few places
in C where it is mandatory, and forgetting it produces a baffling error message on the
following line rather than on the offending one. Members are accessed with a dot:
sv1.name, sv1.age.
2. How it really sits in RAM: structure padding and alignment
Although a struct groups several variables, the compiler does not lay them out in RAM byte after byte. So that the CPU can fetch data from RAM at full speed β typically reading along address-bus lines divisible by 4 or 8 bytes, known as word lines β the compiler applies data alignment rules.
Those rules automatically insert empty bytes, called padding bytes, between the members of a struct.
A. Memory wasted by padding
Consider the following struct:
struct BadStruct {
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
};
Although the real total of the variables is \(1 + 4 + 1 = 6\) bytes,
sizeof(struct BadStruct) returns 12 bytes. Here is the RAM layout the
compiler actually produces for it:
[a (1B)] [padding (3B)] [ b (4B) ] [c (1B)] [padding (3B)] -> Total: 12 Bytes
B. Optimising by reordering fields
To use memory efficiently, simply declare the larger variables first and the smaller ones afterwards:
struct GoodStruct {
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte
};
Now the layout packs the small variables together onto one word line:
[ b (4B) ] [a (1B)] [c (1B)] [padding (2B)] -> Total: 8 Bytes (33% less RAM)
C. Using #pragma pack(1) to remove padding entirely
In embedded systems, or when writing network protocol packets, you need the struct size to be exact to the byte so the data can be sent and received directly. C provides a preprocessor directive that forces the compiler not to insert padding:
#pragma pack(push, 1) // Force 1-byte alignment (removes padding)
struct PackedStruct {
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
};
#pragma pack(pop) // Restore the default alignment
Now sizeof(struct PackedStruct) returns exactly the real 6 bytes. The upside is maximum
RAM savings; the downside is that the CPU reads and writes this struct slightly more slowly, because
the addresses are no longer aligned.
D. Bit-fields
When you need boolean variables or integers with a very small range (say 0 to 7, which is 3 bits),
spending a 32-bit int on each is wasteful. C lets you specify the maximum number of bits
for each member:
struct StatusFlags {
unsigned int isOnline : 1; // Takes exactly 1 bit (0 or 1)
unsigned int role : 3; // Takes exactly 3 bits (values 0 to 7)
unsigned int status : 4; // Takes exactly 4 bits
}; // The whole struct is packed into exactly 1 byte (8 bits)!
3. Using typedef for cleaner code
Declaring a struct variable in the example above always required writing
struct Student sv1; in full, which makes the source verbose.
The typedef (type definition) keyword defines an alias for an existing
type, letting you write more concise code.
#include <stdio.h>
// typedef ket hop struct giup tao ra ten kieu du lieu moi gon sach
typedef struct {
int x;
int y;
} Point;
int main() {
// Khong can ghi chu "struct Point p1;"
Point p1 = {10, 20};
Point p2 = {30, 40};
printf("Toa do diem 1: (%d, %d)\n", p1.x, p1.y);
printf("Toa do diem 2: (%d, %d)\n", p2.x, p2.y);
return 0;
}
The difference shows up at the declaration: without typedef every declaration must spell
out struct Point p;, whereas with it you write just Point p;. For a type you
use hundreds of times across a project that saving adds up β and more importantly, the code reads
closer to a modern language.
4. Structs with pointers and functions
The three sections above all worked with a struct right where it was declared. In real code, though, a struct is almost always passed into a function β and this is where the pointer lesson from Lesson 5 combines with structs to produce something you will meet in every C project.
Recall from Lesson 5: C only has pass by value, so passing a struct into a function copies the whole thing. For a struct holding a 50-character array, every call copies more than 50 bytes β and any change made inside the function lands on the copy and then disappears.
#include <stdio.h>
typedef struct {
char name[50];
int age;
float gpa;
} Student;
// Takes a COPY: changes here never reach the caller
void birthday_by_value(Student s) {
s.age++;
}
// Takes the ADDRESS: -> reaches through the pointer to the real struct
void birthday_by_pointer(Student *s) {
s->age++; // identical to (*s).age++, just far easier to read
}
// const says: I will read this struct, never modify it
void print_student(const Student *s) {
printf("%-6s age=%d gpa=%.2f\n", s->name, s->age, s->gpa);
}
int main() {
Student a = {"An", 20, 3.6f};
birthday_by_value(a);
printf("after by-value: age = %d\n", a.age);
birthday_by_pointer(&a);
printf("after by-pointer: age = %d\n", a.age);
Student students[3] = {{"An", 20, 3.6f}, {"Binh", 21, 3.1f}, {"Chi", 19, 3.9f}};
for (int i = 0; i < 3; i++) {
print_student(&students[i]);
}
return 0;
}
Run it, and the first two lines tell the whole story:
after by-value: age = 20
after by-pointer: age = 21
An age=20 gpa=3.60
Binh age=21 gpa=3.10
Chi age=19 gpa=3.90
The same age++, yet only the pointer version changes the real age. Three things worth
taking from that code:
-
The
->operator is how you reach a member through a pointer. It is just shorthand for(*s).ageβ the parentheses are required, because the dot binds more tightly than*. Writing(*s).ageeverywhere is unbearable, so C provides a dedicated operator, and in real code you will see->far more often than the dot. -
const Student *stells both the reader and the compiler that this function only reads. Accidentally writings->age = 0;inside it becomes a compile error rather than a silent bug. -
An array of structs is declared and iterated exactly like any other array;
&students[i]is the address of elementi. This is how nearly every C program manages a list of records.
const if it only reads. Pass a
copy when the struct is small (a few bytes, such as a coordinate pair) and you want
a guarantee that the function cannot touch the original. A practical rule of thumb: anything bigger
than a pointer or two, pass by address.
5. Struct versus union
A union is defined with syntax identical to a struct, but its storage layout in RAM is completely different:
- Struct: every member gets its own independent region of memory. The struct's size is the sum of its members (possibly plus padding for alignment).
- Union: all members share one single region of memory. A union's size equals the size of its largest member. At any moment you can only store and read one member β writing to one changes or corrupts the values of the others.
Let's run a program comparing their sizes in bytes to make the difference concrete. (Note that the
size of individual types such as int and double, and the padding and
alignment rules, can vary between architectures β see the specifications for
object alignment
and
struct declarations on cppreference.com.)
#include <stdio.h>
typedef struct {
char a; // 1 byte
int b; // 4 bytes
double c; // 8 bytes
} MyStruct;
typedef union {
char a; // 1 byte
int b; // 4 bytes
double c; // 8 bytes
} MyUnion;
int main() {
MyStruct s;
MyUnion u;
// In kich thuoc bo nho (Sizeof)
printf("Kich thuoc Struct: %lu bytes\n", sizeof(s)); // Ket qua: thuong la 16 bytes (do padding)
printf("Kich thuoc Union: %lu bytes\n", sizeof(u)); // Ket qua: dung 8 bytes (kich thuoc cua double c)
// Thu thay doi gia tri trong Union
u.c = 9.87;
printf("u.c = %.2f\n", u.c);
u.b = 100; // Ghi de len phan bo nho chung
printf("u.b = %d\n", u.b);
printf("u.c sau khi ghi de u.b: %.2f\n", u.c); // Gia tri u.c bi thay doi, khong con chinh xac!
return 0;
}
π₯ Download the sample source: struct_vs_union.c
Where unions are used in practice
Unions are common in embedded programming (microcontrollers) and systems programming, wherever RAM is extremely constrained and one variable can represent several different types that never appear at the same time.
union Data { char a; int b; double c; } u;, we assign
u.c = 9.87; and then u.b = 100;. Which statement is the most accurate?
Comments