C Structures: Declaration, Initialization and Union Comparison with Worked Examples

Learn what a structure declaration creates, how each initializer form behaves, and why a union makes a different storage promise. Worked traces cover pointers, copying, padding and tagged unions.

KnowledgeGate Team

Exam prep & CS education

Updated 19 Sep 20265 min read

A structure definition creates a type, while an object declaration allocates an object. Positional, designated and partial initializers set members by different rules. A union reuses one storage region instead of retaining every member simultaneously, so member access, copying, layout and active-member tracking must be reasoned about separately.

C guarantees the order of ordinary structure members and shared storage for union members, but not exact offsets, padding, alignment or sizeof values across all implementations. Every byte count below is illustrative and applies only under the assumptions stated with it.

C structure declaration: define the record before creating objects

c
struct Student {
    int roll;
    char grade;
    float marks;
};

This introduces the tag Student and a structure type with three ordered members, but no object. The separate declaration struct Student first, second; creates and allocates two objects.

In first.marks, Student is the tag used with struct, first is the object, and marks is a member. typedef struct Student Student; creates a type alias, so Student third; declares an object. The alias is not another object or layout.

For wider programming study, use Coding & DSA Courses for Placements.

C structure initialization: positional, designated and partial forms

These three valid forms reach their final values in different ways:

form

declaration

final member values

positional

struct Student a = {17, 'A', 82.5f};

roll=17, grade='A', marks=82.5

designated

struct Student b = {.marks=91.0f, .roll=23, .grade='B'};

roll=23, grade='B', marks=91.0

partial

struct Student c = {.roll=31};

roll=31, grade='\0', marks=0.0

A positional initializer follows declaration order, so it becomes fragile if members are reordered. A designated initializer names each destination and, in C99 and later, may use a different order.

The partial form is initialization. Omitted members are zero-initialized, giving the null character and floating zero shown above. This does not apply to struct Student local; without an initializer at block scope. Its automatic object's members are indeterminate until assigned.

Structure member access: a complete . and -> trace

Now trace an array and a pointer:

c
struct Student batch[2] = {
    {17, 'A', 82.5f},
    {23, 'B', 91.0f}
};
struct Student *p = &batch[1];
p->marks += 1.5f;
float total = batch[0].marks + p->marks;
float average = total / 2.0f;

The pointer targets batch[1], where marks starts at 91.0. The update gives p->marks = 91.0 + 1.5 = 92.5. Then total = 82.5 + 92.5 = 175.0 and average = 175.0 / 2.0 = 87.5.

Use . with a structure object and -> with a pointer to a structure. The expression p->marks is equivalent to (*p).marks. The parentheses matter because . binds more tightly than unary *.

Two batch structure records with pointer p aimed at batch[1], and the average of the two marks computed as 87.5.

Structure copying: member values versus pointer addresses

Whole-structure copying copies each member value:

c
struct Student copy = batch[1];
copy.roll = 99;

Afterward, copy contains (99, 'B', 92.5), while batch[1] remains (23, 'B', 92.5). Scalar members were copied by value.

If a structure contains a pointer, copying it copies the address, not the pointed-to allocation. This is not a deep copy. C also forbids == or != between complete structure objects.

Structure versus union: simultaneous members versus shared storage

The earlier Storage Classes, Structures and Enums in C: Worked Examples for GATE Output and sizeof Questions connects storage classes, enum rules and padding. By contrast, struct Sample retains code, count and mean together; union SampleValue retains only the last-written member, and the tagged-union example then adds a discriminator.

Compare these definitions:

c
struct Sample {
    char code;
    int count;
    float mean;
};

union SampleValue {
    char code;
    int count;
    float mean;
};

struct Sample s = {'K', 25, 6.5f}; retains all three values. For union SampleValue u, after u.count = 25; u.mean = 6.5f;, mean is active and count is not separately retained.

Structure members occupy distinct regions in declaration order, possibly with internal or trailing padding. Union members overlap from the same address. A union is large and aligned enough for every member, possibly with trailing padding. Reading a member other than the last stored is non-portable representation-level behaviour, not safe numeric conversion.

Assume sizeof(char)=1, alignof(char)=1, sizeof(int)=alignof(int)=4, and sizeof(float)=alignof(float)=4. For struct Sample, code is at byte 0, bytes 1 to 3 are padding, count is at bytes 4 to 7, and mean is at bytes 8 to 11, totalling 12 bytes. Every union member starts at byte 0, so its illustrative size is 4 bytes. Another implementation may differ.

Byte-layout maps contrasting the 12-byte struct Sample with its padding against the 4-byte union whose members share byte 0.

Tagged union: pair the shared bytes with an explicit kind

A tagged union records which payload the program should read:

c
enum ValueKind { VALUE_INT, VALUE_FLOAT };

struct Value {
    enum ValueKind kind;
    union {
        int i;
        float f;
    } data;
};

struct Value v = {.kind = VALUE_FLOAT, .data.f = 6.5f};

if (v.kind == VALUE_FLOAT) {
    float current = v.data.f;
}

The discriminator is VALUE_FLOAT, so the matching read is v.data.f. After v.data.i = 25; v.kind = VALUE_INT;, the logical state is kind=VALUE_INT with data.i=25; code should no longer read data.f.

The outer structure keeps the discriminator beside the union, which saves storage for mutually exclusive payloads. A union does not convert an integer to a float. Portable code tracks the active member instead of guessing from overlaid representations.

C structures and unions in exam questions: patterns, traps and solving order

Common practice questions ask whether a declaration defines a type or creates an object, find final initialized values, choose . or ->, calculate layout from stated assumptions, or trace the active union member.

Use this four-step order:

  1. Mark the declared member order.

  2. Write the concrete value of each object member.

  3. Annotate storage duration, or mark the last union write.

  4. Only then compute the output or layout.

Under our assumptions, this gives sizeof(struct Sample) = 12 bytes and sizeof(union SampleValue) = 4 bytes. These are not universal constants.

Repair these common traps:

  • Do not add member sizes without checking alignment and padding.

  • Do not assume an uninitialized automatic object starts at zero.

  • Do not keep a positional initializer unchanged after reordering members.

  • Do not write p.marks when p is a pointer.

  • Do not compare complete structures with ==.

  • Do not treat memcmp as value comparison because padding bytes may differ.

  • Do not read an inactive union member as if it were a numeric cast.

C structures and unions: the short version and next step

  • A structure definition creates a type template.

  • An object declaration allocates an object.

  • Designated initializers reduce mistakes caused by member order.

  • . accesses an object, while -> accesses through a pointer.

  • A structure keeps every member simultaneously.

  • A union shares storage and requires active-member discipline.

In the pointer trace, p->marks=92.5, total=175.0 and average=87.5. Under the stated layout assumptions, the structure is 12 bytes and the union is 4 bytes.

If you want concept teaching followed by practice, continue with the C Language Course: Concepts, MCQs & Coding. If you want a complete C programming route, use the C Programming Course.