Topic 79 of 100 Structures and Unions in C Interactive Tutorial

C Structures: Creating Custom Data Types With struct

13 mins read Last updated: 22 Aug 2026 31,100 views
C Compiler 0.00%

What is C Structures? Core Concept & Direct Summary

Core Objective & Problem Statement

What is a `struct` in C, how does it encapsulate heterogeneous data members, and how do you define and instantiate structures?

Direct Answer Summary

Every data type you've used so far holds one kind of thing. An int is a number. A char is a character. An array holds many values — but all of the same type.

Real-world data isn't like that. A student is a name (string) and an age (int) and a GPA (float). A point is an x-coordinate and a y-coordinate. A product is a name, a price, and a stock count. You can't express "a thing made of these different pieces" with an array — arrays are homogeneous, and reality is not.

Structures solve exactly this. A struct lets you bundle variables of different types together into one named unit — a custom data type that describes a thing rather than a value.

This is the pillar page for our Structures cluster, and it's a genuine turning point in the series: from here on, you'll model problems rather than manipulate variables.

(Suggested image: the hero graphic — a labeled "Student" container holding a char array (name), an int (roll), and a float (gpa), with the whole box treated as one unit named s1)

How Does C Structures Work? In-Depth Mechanics & Architecture

Why Arrays Aren't Enough

Here's the problem structures solve. Suppose you want to store three related facts about a student:

main.c
Run Online
// The scattered approach — three unrelated variableschar name[50];
int roll;
float gpa;

It works for one student. For thirty, you'd need name1, name2, name3... and no way to pass a "student" to a function. Now imagine a function that prints a student — it would need three parameters, and nothing stops a caller from passing mismatched ones:

main.c
Run Online
printStudent(name1, roll7, gpa3);
// ✗ who's checking that these belong together?

Would arrays help? Only if everything were the same type:

main.c
Run Online
float student[3] = {
    3.9, ...
}
;
// ✗ gpa, what? age? You lose all meaning.

Structures give related data a name and a shape:

main.c
Run Online
struct Student {
    char name[50];
    int roll;
    float gpa;
}
;
struct Student s1;
// ONE variable holding all three

Now s1 is a single thing — it can be passed to functions, stored in arrays, returned from them. And the compiler knows its shape, so member access is checked.

(Suggested image: contrast graphic — left: scattered loose variables; right: the same values boxed together in a labeled struct Student container)


Defining a Structure

main.c
Run Online
struct Student {
    char name[50];
    int roll;
    float gpa;
}
;
// ← note the semicolon!

Breaking it down:

  • struct Student — the tag. The type's name is struct Student (both words — a detail that trips everyone at first, and which typedef fixes later on this page).
  • Members (also called fields) listed inside braces, each with its own type
  • A terminating semicolon after the closing brace — required, and one of the most common beginner errors when its absence produces a cryptic message about the next line.

Where to put the definition: before any function that uses it — at the top of your file, or in a header.

main.c
Run Online
#include <stdio.h>
struct Point {
    int x;
    int y;
};
int main(void) {
    struct Point p;
    // declare a variable of this type    p.x = 10;    p.y = 20;
    printf("Point: (%d, %d)\n", p.x, p.y);
    return 0;
}

Output:

A struct definition creates a type, not a variable — like struct Point is now a blueprint. struct Point p; builds an actual instance from it.

(Suggested image: the definition annotated — tag, members, terminating semicolon — with the semicolon called out as the classic trip-up)


All the Ways to Declare and Initialize

Form 1: Declare, then assign members

main.c
Run Online
struct Point p;
p.x = 10;
p.y = 20;

Simple, explicit. Works everywhere.

Form 2: Initialize with braces (in declaration order)

main.c
Run Online
struct Point p = {
    10, 20
}
;
// x = 10, y = 20

Values fill members in the order they're declared. Reorder the members in the definition, and this initializer silently misassigns — a good argument for the designated form next.

Form 3: Designated initializers (C99) — by name

main.c
Run Online
struct Point p = {
    .y = 20, .x = 10
}
;
// order-independent, self-documentingstruct Student s = {
.name = "Alice", .roll = 7, .gpa = 9.1
}
;

Use this form. It's immune to member reordering, it reads clearly, and unspecified members get zeroed:

main.c
Run Online
struct Student s = {
    .name = "Ravi"
}
;
// roll = 0, gpa = 0.0 (rest zeroed)

Form 4: Define and declare at once

main.c
Run Online
struct Point {
    int x, y;
    // multiple members on one line — legal
}
p1, p2;
// variables created right here
// Even without a tag name:struct {
int x, y;
}
temp;
// an anonymous struct type

This works, but it's usually avoided: you can't reuse the type elsewhere, and unnamed structs make passing values around unnecessarily hard. Define the type separately, then declare variables of it — that's the pattern that scales.

Form 5: Copy from another struct

main.c
Run Online
struct Point p = {
    10, 20
}
;
struct Point q;
q = p;
// ✓ legal! whole-struct assignment

This is where structs differ sharply from arrays. Arrays can't be assigned with = (from the pointers vs arrays page), but structs can — this copies every member. And structs of the same type are comparable member by member, though == on structs is still not allowed.

(Suggested image: the five forms as code cards, with Form 3 (designated) marked as recommended)


Accessing Members: The Dot Operator

main.c
Run Online
struct Student s1;
s1.roll = 25;
// assignstrcpy(s1.name, "Priya");                 // strcpy for char arrays — no '=' for arrays!s1.gpa = 8.75;
printf("Name: %s\n", s1.name);
// readprintf("Roll: %d\n", s1.roll);printf("GPA : %.2f\n", s1.gpa);

Output:

main.c
Run Online
Name: PriyaRoll: 25GPA : 8.75

The . operator reaches into a struct variable and selects a member. After that, the member behaves like any variable of its type — s1.roll is an int you can do arithmetic on, s1.name is a char array you can pass to strcpy or printf("%s").

⚠️ Notice the strcpy on line 3.s1.name = "Priya"; is a compile error, because name is an array and arrays aren't assignable — the same rule from the arrays cluster, now appearing inside a struct. Two options:

main.c
Run Online
strcpy(s1.name, "Priya");
// ✓ strcpy (from string.h)// or, if just initializing:struct Student s1 = {
.name = "Priya"
}
;
// ✓ brace initialization works

(Suggested image: the dot operator visualized — an arrow from s1.roll into the specific member box inside the struct)


typedef: Making Struct Names Shorter

Writing struct Student everywhere gets tedious. The typedef creates a shorter alias:

main.c
Run Online
typedef struct {
    char name[50];
    int roll;
    float gpa;
}
Student;

Now the type name is just Student:

main.c
Run Online
Student s1;
// instead of: struct Student s1;
Student class[30];
// arrays of themStudent *ptr;
// pointers to them

Two notes:

  • The struct tag is gone in this form (typedef struct { ... } Student; — an anonymous struct given a name by typedef). That's fine and it's the most common modern style.
  • You can keep the tag if you want both names:typedef struct Student { ... } Student; — useful when the struct needs to reference itself, as in a linked list node:
    main.c
    Run Online
    typedef struct Node {
        int data;
        struct Node *next;
        // ← the tag is required here!
    }
    Node;
    A self-referential struct must use the tagged form, because Node isn't defined yet at the point next is declared. That's a real rule you'll hit the moment you write a linked list.

Recommendation: use typedef from the start. It removes struct noise from every declaration, and it's what most modern C code does.

(Suggested image: "with and without typedef" side-by-side — the same declarations, showing how much shorter and cleaner the typedef version is)


Pointers to Structs and the Arrow Operator

Passing and referring to structs typically happens through pointers (for efficiency — a large struct copied by value is expensive). But then member access needs different syntax, because . has higher precedence than *:

main.c
Run Online
typedef struct {
    int x, y;
}
Point;
Point p = {
    10, 20
};
Point *ptr = &p;
(*ptr).x = 30;
// ✓ works — dereference first, then dot*ptr.x = 30;                              // ✗ ERROR — parses as *(ptr.x)

(*ptr).x is correct but ugly, so C provides a shortcut: the arrow operator->.

main.c
Run Online
ptr->x = 30;
// ✓ exactly the same as (*ptr).xprintf("%d\n", ptr->y);
// 20

When to use which:

SituationSyntax
A struct variables.member
A pointer to a structptr->member
A pointer to a struct, explicit(*ptr).member (identical, rarely written)

Read -> as "points to."student->gpa means "the gpa of the struct that student points to." Once you internalize that, the syntax stops feeling arbitrary — it exists precisely because (*ptr).member is error-prone to type.

(Suggested image: the same struct shown with both a variable name and a pointer, showing . access vs -> access side by side)


Arrays of Structs

This is where structures earn their keep — a collection of records:

main.c
Run Online
#include <stdio.h>
#include <string.h>
#define MAX_STUDENTS 50
typedef struct {
    char name[30];
    int roll;
    float marks[3];
    float average;
}
Student;
int main(void) {
    Student class[MAX_STUDENTS];
    int count = 0;
    // --- Input ---    printf("How many students? ");    scanf("%d", &count);
    if (count < 1 || count > MAX_STUDENTS) {
        puts("Invalid count.");
        return 1;
    }
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
    // flush leftover newline
    for (int i = 0; i < count; i++) {
        printf("\nStudent %d\n", i + 1);
        printf("  Name : ");
        fgets(class[i].name, sizeof(class[i].name), stdin);
        class[i].name[strcspn(class[i].name, "\n")] = '\0';
        printf("  Roll : ");
        scanf("%d", &class[i].roll);
        printf("  Marks (3 subjects): ");
        float total = 0;
        for (int j = 0; j < 3; j++) {
            scanf("%f", &class[i].marks[j]);
            total += class[i].marks[j];
        }
        class[i].average = total / 3;
        while ((c = getchar()) != '\n' && c != EOF);
        // flush again for the next name    }
        // --- Report ---    puts("\n=========== CLASS REPORT ===========");    printf("%-26s %6s %8s %8s %8s %8s\n",           "Name", "Roll", "Sub1", "Sub2", "Sub3", "Avg");    puts("----------------------------------------------------------");
        float classTotal = 0;
        for (int i = 0; i < count; i++) {
            printf("%-26s %6d %8.1f %8.1f %8.1f %8.2f\n", class[i].name, class[i].roll, class[i].marks[0], class[i].marks[1], class[i].marks[2], class[i].average);
            classTotal += class[i].average;
        }
        puts("----------------------------------------------------------");
        printf("Class average: %.2f\n", classTotal / count);
        return 0;
    }

Sample Run:

main.c
Run Online
How many students? 2
Student 1 Name : Priya Sharma Roll : 7 Marks(3 subjects): 85 92 78
Student 2 Name : Ravi Kumar Roll : 12 Marks(3 subjects): 70 65 88
=========== CLASS REPORT ===========Name Roll Sub1 Sub2 Sub3 Avg----------------------------------------------------------Priya Sharma 7 85.0 92.0 78.0 85.00Ravi Kumar 12 70.0 65.0 88.0 74.33----------------------------------------------------------Class average: 79.67

Look what's happening in this program — it's the kind of code you couldn't sanely write before this page:

  • Each element of the array is a complete record — class[i] holds a name, a roll number, three subject marks, and a computed average, all together.
  • Structs nest. A struct member can be an array (marks[3]) or a char array (name) — members are full-fledged variables of any type.
  • class[i].marks[j] — indexing the array of structs, then reaching into a member, then indexing that member. Nested access composes naturally, and the code reads left to right: "the marks of the ith student."
  • The derived field.average is computed once during input and stored, so the report loop doesn't recalculate it. Storing derived values in the struct is a legitimate design choice when they're expensive or used often.

Notice also that this page's material from earlier in the series is doing the work: fgets + strcspn for safe line input, the getchar() flush for the scanf/fgets mixing problem, and printf width specifiers to make the table align. Structures don't replace the earlier tools — they organize them.


Passing Structs to Functions

Two ways, with a real performance difference.

By value — the whole struct is copied:

main.c
Run Online
void printStudent(Student s) {
    // s is a COPY    printf("%s (%d): %.2f\n", s.name, s.roll, s.gpa);
}

Fine for small structs, and safe (the function can't alter your original). But if the struct holds a 200-character buffer plus other fields, every call copies all of it.

By pointer — just the address travels:

main.c
Run Online
void printStudent(const Student *s) {
    // address only — cheap    printf("%s (%d): %.2f\n", s->name, s->roll, s->gpa);
}
printStudent(&s1);
// pass the address

Which to use:

SituationRecommendation
Small struct, function only readsEither — by value is fine and simpler
Large structBy pointer — avoids copying
Function needs to modify the structBy pointer (non-const)
Function reads but doesn't modifyBy pointer with const — cheap and enforced

The professional default: pass struct pointers with const for read-only access. It's the same discipline as the array-parameter and pointer-parameter tutorials, applied to structs. By-value passing is a reasonable exception for genuinely small structs (two or three ints), where a pointer would add indirection for no benefit.

(Suggested image: by-value (a large struct duplicated into the function) vs by-pointer (one arrow) side by side, with the memory cost labeled)


Nested Structures

A struct can contain other structs as members — modeling composition:

main.c
Run Online
typedef struct {
    int day, month, year;
}
Date;
typedef struct {
    char name[30];
    Date birthDate;
    // a struct inside a struct    struct {        int houseNumber;        char street[40];    } address;                             // anonymous inner struct} Person;
    int main(void) {
        Person p = {
            .name = "Priya", .birthDate = {
                .day = 15, .month = 8, .year = 2004
            }
            , .address = {
                .houseNumber = 42, .street = "MG Road"
            }
        };
        printf("Name    : %s\n", p.name);
        printf("Born    : %02d/%02d/%d\n", p.birthDate.day, p.birthDate.month, p.birthDate.year);
        printf("Address : %d, %s\n", p.address.houseNumber, p.address.street);
        return 0;
    }

Output:

main.c
Run Online
Name : PriyaBorn : 15/08/2004Address : 42, MG Road

Nested access just keeps chaining dots: p.birthDate.day reads as "the day of the birthDate of p." That's why structures feel natural — the syntax mirrors the hierarchy you're describing.

Note the anonymous inner struct for address — legal when you don't need to name that type elsewhere. Handy for one-off groupings, and it keeps the type namespace clean.


The One Big Surprise: Structs Aren't Just Stuck Together

Here's something worth knowing early, because it explains a whole category of confusion later:

main.c
Run Online
typedef struct {
    char c;
    // 1 byte
    int i;
    // 4 bytes
}
Mixed;
printf("%zu\n", sizeof(Mixed));
// 8, not 5!

Structs are padded. The compiler inserts invisible bytes between members so each one lands on a memory boundary its type prefers (4-byte ints align to 4-byte boundaries). So sizeof(Mixed) is typically 8, not the 5 you'd expect from adding 1 + 4.

Why this matters:

  • sizeof on a struct includes padding — never assume it equals the sum of its members
  • Writing a struct to a file with fwrite writes padding bytes too, which may differ between compilers/platforms — so struct file formats aren't portable
  • Member order affects the total size (putting larger members first can reduce padding)

This gets its full treatment in the structures cluster later. For now, the headline: sizeof(struct) ≥ the sum of its members.


Common Structure Mistakes

  1. Forgetting the semicolon after the closing brace — the error message often points at the next line, sending you hunting in the wrong place.
  2. Writing struct Student but declaring just Student (or vice versa) — if you typedef'd it, drop the struct; if you didn't, keep it. Choose one approach and be consistent.
  3. Using = on a char array member — s.name = "Priya"; is a compile error. Use strcpy or brace initialization.
  4. Using . on a struct pointer — ptr.x doesn't compile. Pointers use ->, or (*ptr).x.
  5. Comparing structs with == — not allowed in C. Compare member by member (and use strcmp for char arrays inside).
  6. Forgetting that arrays in structs aren't copyable by assignment — struct assignment q = p copies everything including array members. But q.name = p.name doesn't work.
  7. Assuming sizeof equals the sum of members — padding. Check sizeof if the size matters.
  8. Passing large structs by value in hot code — silent performance cost from all that copying. Pass pointers.
  9. Reading an uninitialized struct — uninitialized members hold garbage, just like uninitialized variables. Initialize with braces or designated initializers.
  10. Trying to printf a whole struct — there's no format specifier for a struct. Print its members individually.

(Suggested image: "10 Struct Mistakes" checklist graphic — consistent with the series, with #1 and #4 highlighted)


Final Thoughts

Structures are the moment C stops being a language of values and starts being a language of things. An int is a number; a struct Student is a student. That shift — from primitive types to domain types — is what makes larger programs comprehensible, because the code starts describing the problem instead of the machine.

The core material is compact: define with struct, name your type with typedef, reach members with ., reach members through pointers with ->, initialize with designated initializers, and pass large structs by const pointer. That's the working vocabulary, and it covers nearly everything you'll write.

Two bigger ideas to carry forward: structures compose (structs containing structs, arrays of structs, pointers to structs), and your own types can be as natural to use as built-in ones. Once Student and Point and Date are types in your program, the code starts reading like the problem statement — which is the whole goal.

Next in this cluster: typedef and struct design in depth, then nested structures, arrays of structs, struct pointers, and self-referential structures (the gateway to linked lists).

Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
typedef struct {
    int id;
    char title[64];
    double price;
}
Book;
int main() {
    Book myBook = {
        101, "The C Programming Language", 45.50
    };
    printf("Book ID:    %d\
", myBook.id);
    printf("Title:      %s\
", myBook.title);
    printf("Price:      $%.2f\
", myBook.price);
    printf("Struct Size:%zu bytes\
", sizeof(Book));
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Order struct members from largest to smallest byte size (e.g., double, int, short, char) to minimize compiler padding waste.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: What is structure padding and alignment in C?

A: Modern CPUs access memory more efficiently when data types reside at memory addresses that are multiples of their size (e.g. 4-byte `int` on 4-byte boundaries). Compilers insert silent padding bytes between struct members to satisfy alignment.
Q

Q: Can structures be compared directly using `==` in C?

A: No, because structure padding bytes may contain random garbage values. You must compare individual struct members or write a custom comparator.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations