Structures & Pointers — Deep Dive
0%
Structures  ·  Pointers  ·  Deep Explanation

Structures & Pointers in C —
Fully Explained

Not just code — every concept explained from first principles. What a struct actually is in memory. What a pointer really holds. Why -> exists. Five progressively deeper examples with full diagrams and plain-English breakdowns.

1
What is a Struct?
2
Pointer to Struct
3
Arrow Operator
4
Pointer in Function
5
Array of Structs
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: 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:
  • struct keyword — tells the compiler you are defining a compound type.
  • typedef — creates an alias so you can write Student instead of struct Student everywhere.
  • Field declarations inside { } — each field gets its own type and name. Fields are stored in the order declared.
You access fields with the dot operator .: 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
name[20] char × 20 = 20 bytes 0x1000 roll int = 4 bytes 0x1014 marks float = 4 bytes 0x1018 grade char = 1 byte 0x101c padding 3 bytes sizeof(Student) = 28 bytes total (20+4+4+1+3 padding) Student s — starts at address 0x1000
ex1_struct_basics.c
C
#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;
}
output
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: 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
Student *p 0x7ffd1000 8 bytes (address) stored at 0x7ffd2000 p points to (*p) to access Student s (at 0x7ffd1000) name "Ananta" roll 101 marks 88.5 grade 'B'
ex2_pointer_to_struct.c
C
#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;
}
output
--- 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 (*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.
ExpressionWhat you haveMeaningEquivalent
s.marksstruct variableDirectly access the field— (direct)
(*p).markspointer to structDereference, then access fieldsame as p->marks
p->markspointer to structArrow: shorthand for (*p).markssame as (*p).marks
p->marks = 95pointer to structWrite field through pointer(*p).marks = 95
ex3_arrow_operator.c
C
#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;
}
output
--- 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 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
PASS BY VALUE — f(s) Student s original copy Student s (copy) inside func changes stay local original unchanged PASS BY POINTER — f(&s) Student s original &s Student *p 8 bytes only p->field writes go through original changes
ex4_ptr_in_function.c
C
#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;
}
output
=== 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 — 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.
arr[0] *(arr+0) "Ananta" | 88.5 0x2000 arr[1] *(arr+1) "Priya" | 95.0 0x201c (= 0x2000 + 28) arr[2] *(arr+2) "Rahul" | 72.0 0x2038 (= 0x2000 + 56) arr+1 skips 28 bytes (sizeof Student)
ex5_array_pointers_deep.c
C
#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;
}
output
=== 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 = &s stores the address. sizeof(p) == 8 always (64-bit). (*p) is the full struct. Writing (*p).field = val changes the original. Uninitialised pointer = undefined behaviour.
  • Ex 3 — Arrow operator: p->field is identical to (*p).field. Use . with structs, -> with pointers. Nested: p->addr.city (addr is struct) vs p->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 by sizeof(struct) automatically. Return &arr[i] for zero-copy access. Never return pointer to local variable.