1
📚 What is a Structure? — Memory Layout Explained
A struct groups different types under one name — all fields live in a single block of memory
Foundations
Plain-English Concept
Imagine you need to store a student's details — name, roll number, marks, and grade. Without a struct you'd need four separate variables:
In memory, all the fields of a struct sit in a single contiguous block — one right after the other (with possible alignment padding between them). The struct's address is the address of its very first field.
name, roll, marks, grade. For 100 students that's 400 variables. A struct bundles all four into a single named unit. You create one Student type, and then declare as many Student variables as you need — each one is a self-contained package of all four fields.
In memory, all the fields of a struct sit in a single contiguous block — one right after the other (with possible alignment padding between them). The struct's address is the address of its very first field.
Syntax breakdown — three things happen in one declaration:
structkeyword — tells the compiler you are defining a compound type.typedef— creates an alias so you can writeStudentinstead ofstruct Studenteverywhere.- Field declarations inside
{ }— each field gets its own type and name. Fields are stored in the order declared.
.: s.name, s.roll, s.marks. The dot literally means "inside this struct variable, find the field named …".
how Student s sits in memory — each field has an address
- 1Define the type once with
typedef struct { ... } Student;— this tells the compiler what fields every Student variable will have. - 2Declare a variable:
Student s;— the compiler allocates a block of memory large enough for all fields. - 3Initialise with braces:
Student s = {"Ananta", 101, 88.5f, 'B'};— fields are filled left-to-right in declaration order. - 4Access fields with dot:
s.marksreads the float.s.roll = 202;writes to the int. Each field is an independent variable inside the struct. - 5sizeof:
sizeof(Student)returns the total bytes including any alignment padding the compiler adds between fields.
#include <stdio.h> #include <string.h> /* Step 1 — define the type */ typedef struct { char name[20]; /* 20 bytes */ int roll; /* 4 bytes */ float marks; /* 4 bytes */ char grade; /* 1 byte */ } Student; /* total: 28 bytes (with 3-byte padding) */ int main() { /* Step 2+3 — declare and initialise */ Student s = {"Ananta", 101, 88.5f, 'B'}; /* Step 4 — access with dot operator */ printf("Name : %s\n", s.name); printf("Roll : %d\n", s.roll); printf("Marks : %.1f\n", s.marks); printf("Grade : %c\n", s.grade); /* Step 5 — sizes and addresses */ printf("\nsizeof(Student) = %zu bytes\n", sizeof(Student)); printf("Address of s = %p\n", (void*)&s); printf("Address of s.name = %p\n", (void*)&s.name); printf("Address of s.roll = %p\n", (void*)&s.roll); printf("Address of s.marks = %p\n", (void*)&s.marks); printf("Address of s.grade = %p\n", (void*)&s.grade); /* Modify individual fields */ s.marks = 95.0f; s.grade = 'A'; printf("\nAfter update: Marks=%.1f Grade=%c\n", s.marks, s.grade); /* Struct assignment copies ALL fields at once */ Student s2 = s; printf("Copy s2.name = %s (independent copy)\n", s2.name); return 0; }
Name : Ananta Roll : 101 Marks : 88.5 Grade : B sizeof(Student) = 28 bytes Address of s = 0x7ffd1000 Address of s.name = 0x7ffd1000 ← same as struct — first field Address of s.roll = 0x7ffd1014 ← 20 bytes after start Address of s.marks = 0x7ffd1018 ← 4 bytes after roll Address of s.grade = 0x7ffd101c ← 4 bytes after marks After update: Marks=95.0 Grade=A Copy s2.name = Ananta (independent copy)
Key insight from the addresses:
&s == &s.name — the struct and its first field share the same starting address. Roll is 20 bytes later (0x1014 = 0x1000 + 20). This confirms fields are laid out sequentially in memory, exactly in declaration order.Struct assignment
s2 = s copies every field byte-for-byte — including the name array. After the assignment, s and s2 are completely independent. Changing s.marks does not affect s2.marks. This is called a shallow copy — fine for plain data, but watch out if fields contain pointers to heap memory.example 2
2
📌 What is a Pointer to a Struct? — Address, Dereference, Size
A pointer holds the address of a struct — not a copy, not the data — just the location
Pointer to Struct
Plain-English Concept
A pointer is a variable that stores a memory address. A pointer to a struct stores the address of where that struct lives in memory — not the struct data itself. Think of a struct as a house and a pointer as the house's postal address written on a piece of paper. Handing someone the piece of paper (the pointer) lets them find and enter the house (the struct) without you carrying the entire house to them.
Declare:
Declare:
Student *p; — p is a pointer that can hold the address of a Student struct. Assign: p = &s; — now p holds the address of s. Dereference: (*p) — follow the address, get the actual struct. Then use dot: (*p).marks — read the marks field of the struct p points to.
pointer p stores address of struct s — two variables, one struct
- 1Declare pointer:
Student *p;—pcan hold the address of anyStudent. Not initialised yet — dangerous to use. - 2Assign address:
p = &s;— the&(address-of) operator gives the memory address ofs. Nowpsafely holds it. - 3Dereference:
*p— the*(dereference) operator follows the address and gives you the whole struct.(*p)is aStudent. - 4Access fields:
(*p).marks— dereference then dot. Parentheses are required because.binds tighter than*. - 5Write through pointer:
(*p).marks = 95;— this changess.marksdirectly. The pointer and the struct share the same memory.
#include <stdio.h> typedef struct { char name[20]; int roll; float marks; char grade; } Student; int main() { Student s = {"Ananta", 101, 88.5f, 'B'}; Student *p = &s; /* p holds the address of s */ printf("--- Proof p points to s ---\n"); printf("Address of s : %p\n", (void*)&s); printf("Value of p : %p\n", (void*)p); printf("Same? %s\n\n", (&s == p) ? "YES" : "NO"); printf("sizeof(Student) = %zu bytes\n", sizeof(Student)); printf("sizeof(p) = %zu bytes (pointer is always 8)\n\n", sizeof(p)); /* Reading via dereference — (*p) is the full struct */ printf("--- Reading via (*p).field ---\n"); printf("(*p).name = %s\n", (*p).name); printf("(*p).roll = %d\n", (*p).roll); printf("(*p).marks = %.1f\n", (*p).marks); /* Writing via dereference — changes the real s */ printf("\n--- Writing (*p).marks = 96.0 ---\n"); (*p).marks = 96.0f; printf("s.marks after write = %.1f\n", s.marks); /* s changed too! */ /* Pointer arithmetic — p+1 skips sizeof(Student) bytes */ printf("\n--- Pointer arithmetic ---\n"); printf("p = %p\n", (void*)p); printf("p+1 = %p (jumps %zu bytes)\n", (void*)(p+1), sizeof(Student)); return 0; }
--- Proof p points to s --- Address of s : 0x7ffd1000 Value of p : 0x7ffd1000 Same? YES sizeof(Student) = 28 bytes sizeof(p) = 8 bytes (pointer is always 8) --- Reading via (*p).field --- (*p).name = Ananta (*p).roll = 101 (*p).marks = 88.5 --- Writing (*p).marks = 96.0 --- s.marks after write = 96.0 --- Pointer arithmetic --- p = 0x7ffd1000 p+1 = 0x7ffd101c (jumps 28 bytes)
sizeof(p) is always 8 bytes on a 64-bit system — regardless of how big the struct is. A pointer is just an integer holding an address. Passing a pointer to a function always costs 8 bytes of data, whether the struct is 28 bytes or 28,000 bytes.
Never use an uninitialised pointer.
Student *p; contains garbage — pointing it anywhere and dereferencing it causes undefined behaviour (segfault, data corruption). Always initialise: either p = &s (existing variable) or p = malloc(sizeof(Student)) (heap) or p = NULL (safe sentinel).example 3
3
➡ The Arrow Operator — Why -> Exists and How It Works
p->field is exactly (*p).field — shorthand that makes pointer code readable
Arrow Operator
Plain-English Concept
In Example 2 you wrote
Rule: Use
(*p).marks — parentheses required because the . operator binds tighter than *. This pattern is so common in C that a dedicated shorthand was created: the arrow operator ->. p->marks means exactly the same as (*p).marks — dereference the pointer, then access the field. Every time you see ->, read it as "follow the pointer, then look inside."
Rule: Use
. when you have the struct itself. Use -> when you have a pointer to the struct. You will use -> constantly once you start passing structs to functions.
| Expression | What you have | Meaning | Equivalent |
|---|---|---|---|
| s.marks | struct variable | Directly access the field | — (direct) |
| (*p).marks | pointer to struct | Dereference, then access field | same as p->marks |
| p->marks | pointer to struct | Arrow: shorthand for (*p).marks | same as (*p).marks |
| p->marks = 95 | pointer to struct | Write field through pointer | (*p).marks = 95 |
- 1If
pis aStudent*, thenp->namereads thenamefield of the structppoints to. - 2
p->marks = 90writes 90 into themarksfield of the struct at addressp. The original variable changes. - 3Chained arrow: if a struct contains another struct pointer, access deeply:
p->addr->cityfollows two pointers. - 4
p->roll++is equivalent to(*p).roll = (*p).roll + 1— increment the field in-place through the pointer.
#include <stdio.h> #include <string.h> typedef struct { char city[20]; int pin; } Address; typedef struct { char name[20]; int age; float salary; Address addr; /* nested struct (not a pointer) */ } Employee; void showEquivalence(Employee *p) { printf("--- Arrow vs (*p).field ---\n"); /* These four pairs are IDENTICAL */ printf("p->name = %s\n", p->name); printf("(*p).name = %s\n", (*p).name); printf("p->salary = %.2f\n", p->salary); printf("(*p).salary = %.2f\n", (*p).salary); printf("\n--- Nested struct (addr is NOT a pointer) ---\n"); printf("p->addr.city = %s\n", p->addr.city); /* Arrow then dot: p->addr is the Address struct, .city accesses it */ } void giveRaise(Employee *p, float pct) { p->salary += p->salary * pct / 100.0f; /* modify through arrow */ p->age++; /* increment via arrow */ printf("After raise: salary=%.2f age=%d\n", p->salary, p->age); } int main() { Employee e = {"Priya", 28, 50000.0f, {"Haridwar", 249401}}; Employee *p = &e; /* p points to e */ showEquivalence(p); printf("\n--- Writing through arrow ---\n"); printf("Before: salary=%.2f\n", e.salary); giveRaise(p, 20); /* 20% raise */ printf("e.salary is now: %.2f\n", e.salary); /* original changed */ printf("\n--- Change city via nested arrow+dot ---\n"); strcpy(p->addr.city, "Dehradun"); /* p->addr is struct, .city is field */ printf("City now: %s\n", e.addr.city); /* original updated */ return 0; }
--- Arrow vs (*p).field --- p->name = Priya (*p).name = Priya p->salary = 50000.00 (*p).salary = 50000.00 --- Nested struct (addr is NOT a pointer) --- p->addr.city = Haridwar --- Writing through arrow --- Before: salary=50000.00 After raise: salary=60000.00 age=29 e.salary is now: 60000.00 --- Change city via nested arrow+dot --- City now: Dehradun
Arrow then dot rule:
p->addr.city — use -> to go through the pointer to reach addr (a nested struct, not a pointer). Then use . to access city inside that nested struct. If addr were itself a pointer, you'd write p->addr->city — two arrows.example 4
4
🛠️ Passing Struct Pointer to a Function — Value vs Pointer
By value = safe copy. By pointer = touches original. const pointer = safe read-only.
Pointers in Functions
Plain-English Concept
When you call a function in C, arguments are always passed by value — the function gets its own private copy. For structs this means the entire struct is duplicated every call. For a 28-byte
Passing a pointer to the struct solves both problems: only 8 bytes are copied (the address), and the function can reach through the pointer to modify the real struct. Add
Student that's fine. For a 2,800-byte struct with arrays, it's expensive. More critically, if you want the function to change the original, a copy won't do — the original never changes.Passing a pointer to the struct solves both problems: only 8 bytes are copied (the address), and the function can reach through the pointer to modify the real struct. Add
const to the pointer parameter when the function only needs to read — the compiler then enforces that no field is written, documenting intent and catching bugs at compile time.
pass by value vs pass by pointer — what happens to memory
- 1By value
void f(Student s): function gets a complete copy. Changes inside stay local. Original struct is safe but cannot be updated. - 2By pointer
void f(Student *p): function gets the address. Usep->field = ...to write into the original. Changes survive after the function returns. - 3Const pointer
void f(const Student *p): function gets the address but cannot write. Compiler enforces read-only. Use for display/print/calculate functions. - 4Return struct
Student f(...): function builds a local struct and returns it. The caller receives a copy of the returned struct. Clean factory pattern.
#include <stdio.h> #include <string.h> typedef struct { char name[20]; int roll; float marks; } Student; /* === BY VALUE === changes do NOT reach caller */ void tryChangeByValue(Student s, float newMarks) { s.marks = newMarks; /* only changes the LOCAL copy */ printf(" Inside (by value) : marks = %.1f\n", s.marks); } /* === BY POINTER === changes DO reach caller */ void changeByPointer(Student *p, float newMarks) { p->marks = newMarks; /* writes into original through pointer */ printf(" Inside (by ptr) : marks = %.1f\n", p->marks); } /* === CONST POINTER === read-only — compiler enforces */ void displayReadOnly(const Student *p) { printf(" Display: %-10s Roll:%-4d Marks:%.1f\n", p->name, p->roll, p->marks); /* p->marks = 0; ← COMPILE ERROR: cannot modify const */ } /* === RETURN STRUCT === factory pattern */ Student makeStudent(const char *nm, int roll, float marks) { Student s; strncpy(s.name, nm, 19); s.name[19] = '\0'; s.roll = roll; s.marks = marks; return s; /* returned struct copied to caller */ } int main() { Student s = makeStudent("Ananta", 101, 72.0f); printf("=== Original ===\n"); displayReadOnly(&s); printf("\n=== Pass By Value (tryChangeByValue) ===\n"); tryChangeByValue(s, 95.0f); printf(" After call: s.marks = %.1f (unchanged)\n", s.marks); printf("\n=== Pass By Pointer (changeByPointer) ===\n"); changeByPointer(&s, 95.0f); printf(" After call: s.marks = %.1f (changed!)\n", s.marks); printf("\n=== Final state ===\n"); displayReadOnly(&s); printf("\n=== Factory — return struct ===\n"); Student s2 = makeStudent("Priya", 102, 88.0f); displayReadOnly(&s2); return 0; }
=== Original === Display: Ananta Roll:101 Marks:72.0 === Pass By Value (tryChangeByValue) === Inside (by value) : marks = 95.0 After call: s.marks = 72.0 (unchanged) === Pass By Pointer (changeByPointer) === Inside (by ptr) : marks = 95.0 After call: s.marks = 95.0 (changed!) === Final state === Display: Ananta Roll:101 Marks:95.0 === Factory — return struct === Display: Priya Roll:102 Marks:88.0
The output proves the difference. After
tryChangeByValue, s.marks is still 72.0 — the function's copy changed, not the original. After changeByPointer, s.marks is 95.0 — the pointer reached through to the original. This is the single most important concept in passing structs to functions in C.example 5
5
📊 Array of Structs + Pointer Arithmetic — Complete Deep Dive
How arrays decay, pointer arithmetic on structs, search and modify — everything connected
Array & Pointer Arithmetic
Plain-English Concept
An array of structs is just a row of struct-sized boxes laid end to end in memory. When you pass the array to a function, it decays into a pointer to its first element —
Functions that return a pointer into the array (like a topper-finder) give the caller direct access to the actual element — no copying. Modifying through that pointer modifies the real array element.
Student arr[] and Student *arr are identical inside a function. Inside the function, arr[i] is exactly *(arr + i) — the compiler multiplies i by sizeof(Student) and adds it to the base address to find element i. This is pointer arithmetic in action.
Functions that return a pointer into the array (like a topper-finder) give the caller direct access to the actual element — no copying. Modifying through that pointer modifies the real array element.
array of structs — arr[0], arr[1], arr[2] in memory. arr+1 skips sizeof(Student) bytes.
- 1Array decays to pointer: passing
batchto a function is like passing&batch[0]. The function receives the address of the first element. - 2arr[i] = *(arr + i): indexing is pointer arithmetic. The compiler computes
i * sizeof(Student)and adds to the base address to find element i. - 3arr[i].field = (arr+i)->field: both expressions access the same field of the same element. Use whichever is clearer — they compile to identical machine code.
- 4Return pointer into array:
return &arr[i]gives the caller a pointer to the actual element inside the array — zero copying, and modifying through the pointer changes the real array. - 5Never return pointer to local variable: a local struct inside a function is destroyed when the function returns. The pointer becomes dangling — undefined behaviour.
#include <stdio.h> #include <string.h> typedef struct { char name[20]; int roll; float marks; } Student; /* printAll — arr decays to pointer; arr[i] == *(arr+i) */ void printAll(const Student *arr, int n) { printf(" %-12s %5s %7s\n", "Name", "Roll", "Marks"); printf(" --------------------------\n"); for (int i = 0; i < n; i++) { /* arr[i].name == (arr+i)->name — identical */ printf(" %-12s %5d %7.1f\n", arr[i].name, arr[i].roll, arr[i].marks); } } /* Show pointer arithmetic explicitly */ void showArithmetic(const Student *arr, int n) { printf(" Base address arr = %p\n", (void*)arr); printf(" sizeof(Student) = %zu bytes\n\n", sizeof(Student)); for (int i = 0; i < n; i++) { printf(" arr+%d = %p arr[%d].name = %s\n", i, (void*)(arr+i), i, (arr+i)->name); } } /* Returns pointer to highest-marks element — no copy */ Student* findTopper(Student *arr, int n) { Student *best = arr; /* start: point at arr[0] */ for (int i = 1; i < n; i++) if (arr[i].marks > best->marks) best = arr + i; /* pointer arithmetic */ return best; /* pointer into the real array */ } /* Apply bonus to every element via pointer walk */ void applyBonus(Student *arr, int n, float bonus) { for (int i = 0; i < n; i++, arr++) /* arr++ moves to next struct */ arr->marks += bonus; } int main() { Student batch[] = { {"Ananta", 101, 88.5f}, {"Priya", 102, 95.0f}, {"Rahul", 103, 72.0f}, {"Sneha", 104, 91.5f}, {"Vikram", 105, 45.0f} }; int n = 5; printf("=== All Students ===\n"); printAll(batch, n); printf("\n=== Pointer Arithmetic Demo ===\n"); showArithmetic(batch, n); printf("\n=== Topper (returned pointer) ===\n"); Student *top = findTopper(batch, n); printf(" Topper: %s Marks: %.1f\n", top->name, top->marks); /* Modify through returned pointer — changes real array */ top->marks += 2.0f; printf(" After 2 bonus marks: %s = %.1f\n", top->name, top->marks); printf(" batch[1].marks (same element): %.1f\n", batch[1].marks); /* proves top pointed to batch[1] */ printf("\n=== Apply 5-mark bonus to all ===\n"); applyBonus(batch, n, 5.0f); printAll(batch, n); return 0; }
=== All Students === Name Roll Marks -------------------------- Ananta 101 88.5 Priya 102 95.0 Rahul 103 72.0 Sneha 104 91.5 Vikram 105 45.0 === Pointer Arithmetic Demo === Base address arr = 0x7ffd2000 sizeof(Student) = 28 bytes arr+0 = 0x7ffd2000 arr[0].name = Ananta arr+1 = 0x7ffd201c arr[1].name = Priya arr+2 = 0x7ffd2038 arr[2].name = Rahul arr+3 = 0x7ffd2054 arr[3].name = Sneha arr+4 = 0x7ffd2070 arr[4].name = Vikram === Topper (returned pointer) === Topper: Priya Marks: 95.0 After 2 bonus marks: Priya = 97.0 batch[1].marks (same element): 97.0 === Apply 5-mark bonus to all === Name Roll Marks -------------------------- Ananta 101 93.5 Priya 102 102.0 Rahul 103 77.0 Sneha 104 96.5 Vikram 105 50.0
Pointer arithmetic proof:
arr+0 = 0x2000, arr+1 = 0x201c. Difference = 0x1c = 28 bytes = sizeof(Student). The compiler automatically scales pointer addition by the size of the pointed-to type. arr[i] is not magic — it is *(arr + i * sizeof(Student)) under the hood.The topper proof:
top->marks += 2 and batch[1].marks both show 97.0 — they are the same memory location. top is not a copy of Priya's record; it is a pointer directly into the batch array. Modifying through top modifies batch[1] directly.Never do this:
Student* f() { Student local = {...}; return &local; } — returning a pointer to a local variable. When f returns, local is destroyed. The pointer is dangling — reading or writing through it is undefined behaviour. Only return pointers to heap memory (malloc) or to data that lives in the caller (array elements, global variables).checklist — tick each concept when you understand it
- Ex 1 — Struct in memory: Fields are laid out sequentially.
&s == &s.first_field.sizeof(struct)includes alignment padding. Struct assignment (s2 = s) copies every byte. - Ex 2 — Pointer to struct:
Student *p = &sstores the address.sizeof(p) == 8always (64-bit).(*p)is the full struct. Writing(*p).field = valchanges the original. Uninitialised pointer = undefined behaviour. - Ex 3 — Arrow operator:
p->fieldis identical to(*p).field. Use.with structs,->with pointers. Nested:p->addr.city(addr is struct) vsp->addr->city(addr is pointer). - Ex 4 — Passing to functions: By value = local copy, original safe, cannot update. By pointer = 8 bytes sent, writes reach original via
p->field.const Student *p= read-only, compiler enforced. Factory pattern returns struct by value. - Ex 5 — Array of structs + arithmetic: Array name decays to pointer to first element.
arr[i] == *(arr+i). Pointer arithmetic scales bysizeof(struct)automatically. Return&arr[i]for zero-copy access. Never return pointer to local variable.