Help improve SovranCode?

Google Analytics can measure anonymous usage after you choose Allow. Essential account and progress features work either way.

SovranCode
C: Foundations for Systems Programming Structures, Enumerations, and Unions
This device
Course contentsStructures, Enumerations, and Unions · 15 titles

C foundations

What C Is and How C Programs RunYour First C ProgramVariables, Types, and OperatorsInput and Output with printf and scanfConditions and Loops

Program structure and data

Functions, Headers, and ScopeArrays and StringsPointers and Memory AddressesStructures, Enumerations, and UnionsFiles and Command-Line Arguments

Memory, tooling, and practice

Dynamic Memory AllocationPreprocessor Directives and Separate CompilationErrors, Debugging, and Undefined BehaviorBuild Tools and MakefilesProject: Command-Line Inventory Manager
C: Foundations for Systems Programming15 complete lessons

C foundations

What C Is and How C Programs RunYour First C ProgramVariables, Types, and OperatorsInput and Output with printf and scanfConditions and Loops

Program structure and data

Functions, Headers, and ScopeArrays and StringsPointers and Memory AddressesStructures, Enumerations, and UnionsFiles and Command-Line Arguments

Memory, tooling, and practice

Dynamic Memory AllocationPreprocessor Directives and Separate CompilationErrors, Debugging, and Undefined BehaviorBuild Tools and MakefilesProject: Command-Line Inventory Manager
PREVIOUS LESSONPointers and Memory Addresses
NEXT LESSONFiles and Command-Line Arguments
Program structure and data 165 min

Structures, Enumerations, and Unions

Good C programs do not merely store bytes; they name relationships and make each valid state obvious. Structures group fields that belong together, enumerations label a closed set of choices, and unions save storage when one object can validly take one of several forms.

What you will leave with

You will define and initialize structure types, select fields through objects and pointers, pass structures with clear ownership rules, create explicit enum values, design exhaustive state handling, explain why a union needs its own active-member contract, and build a small tagged-union record without guessing at its stored representation.

Choose the representation that matches the promise

STRUCTAll fields belong togetherA product has a code, price, and stock count at the same time.
ENUMOne named choiceAn order is pending, paid, shipped, or cancelled—not an unexplained number.
UNIONOne of several formsA configuration value is currently an integer, a decimal, or a short label.
TAGState the active formAn enum beside a union records which member code may read.

Structures model one coherent record

Use a struct when several values describe one thing. The declaration defines a new shape; a variable of that type reserves storage for every field. Unlike an array, fields can have different types and are selected by name.

struct Product {
  char code[12];
  double price;
  int stock;
};

int main(void) {
  struct Product keyboard = {"KB-104", 49.99, 12};
  keyboard.stock = keyboard.stock - 1;
  printf("%s: %.2f (%d left)\n", keyboard.code, keyboard.price, keyboard.stock);
  return 0;
}

struct Product is the type name here, and keyboard is one object with all three fields. Use the dot operator for an object: keyboard.price. A field is not a separate global variable; it belongs to the structure object selected before the dot.

Start from an invariant, not a syntax trick

Before defining a structure, say what must remain true. For this product, stock should not become negative, code should fit the capacity including its trailing \0, and price should have a documented currency and unit. A type groups fields; your functions enforce their meaning.

C DATA-MODEL RUNNER

Run a record update through named fields

This bounded runner reads the Reading record, applies the visible field update, and prints the resulting record. Edit the designated initializer or the value update, then run it again.

#include <stdio.h>

struct Reading {
  int id;
  double value;
};

int main(void) {
  struct Reading reading = {
    .id = 7,
    .value = 18.5,
  };

  reading.value = reading.value + 1.5;
  printf("Reading %d: %.1f\\n", reading.id, reading.value);
  return 0;
}
TERMINAL OUTPUT
Edit the model, predict the result, then run it.

This safe browser runner executes a deliberately narrow C data-model exercise. It never runs native code: it validates the lesson's struct contract and evaluates only the visible record, enum, or union state.

Try this next: Change .id, .value, and the amount added to reading.value. Keep field names and the struct Reading contract intact.

Initialize records deliberately

Positional initialization follows field order and becomes fragile when the type evolves. C also supports designated initializers, which name each target field and are easier to review.

struct Product keyboard = {
  .code = "KB-104",
  .price = 49.99,
  .stock = 12,
};

Prefer designated initializers in examples, tests, and long-lived code where a field-order mistake would be expensive. The compiler can then connect each supplied value to its intended field. A member you omit receives zero initialization when the object has static storage or when you use an initializer for an automatic aggregate; still make important defaults visible in code.

Use dot for an object and arrow for a pointer

When a function receives the address of a structure, it has a pointer. The arrow operator selects a member through that pointer: product->stock means (*product).stock. The parentheses in the expanded form are essential because dot binds before unary *.

int sell_one(struct Product *product) {
  if (product == NULL || product->stock <= 0) {
    return 0;
  }

  product->stock -= 1;
  return 1;
}

void print_product(const struct Product *product) {
  if (product == NULL) {
    return;
  }
  printf("%s: %.2f\n", product->code, product->price);
}
copy

struct Product product

The function receives a complete copy. Small read-only records can be passed this way, but changes do not update the caller.

mutate

struct Product *product

The function may update the caller-owned record after validating the pointer and its field invariants.

inspect

const struct Product *product

The function avoids a copy and promises not to change fields through this access path.

Arrays of structures keep related fields aligned

An array of structures is often safer than parallel arrays because each row of data travels as one object. With struct Product inventory[3], inventory[index].price and inventory[index].stock refer to fields from the same product at the same index.

double inventory_value(const struct Product products[], int count) {
  double total = 0.0;

  for (int index = 0; index < count; index++) {
    total += products[index].price * products[index].stock;
  }
  return total;
}

As with every array parameter, products converts to a pointer and does not retain its length. Keep the count in the contract, validate it before the loop, and use the same bound for every field access.

Enumerations give states readable names

An enum declares integral constants with domain-specific names. It makes control flow easier to read and lets you avoid unexplained literals such as 3 scattered through your program.

enum OrderStatus {
  ORDER_PENDING,
  ORDER_PAID,
  ORDER_SHIPPED,
  ORDER_CANCELLED,
};

const char *status_label(enum OrderStatus status) {
  switch (status) {
    case ORDER_PENDING: return "pending";
    case ORDER_PAID: return "paid";
    case ORDER_SHIPPED: return "shipped";
    case ORDER_CANCELLED: return "cancelled";
    default: return "invalid status";
  }
}

The default underlying values begin at zero and increase by one, but your code should rely on named constants—not their current numbers. Assign explicit values only when an external format, protocol, or stored data contract requires stable numeric codes.

An enum does not make invalid bytes impossible

C can still receive an unexpected value from a file, network packet, cast, or uninitialized object. Keep a default branch when input can cross a trust boundary, and make the recovery behavior intentional.

C DATA-MODEL RUNNER

Run a named state instead of a magic number

The runner validates the declared ConnectionState names and reports the active enum constant with its current ordinal. It makes visible why names are the stable contract.

#include <stdio.h>

enum ConnectionState {
  CONNECTION_IDLE,
  CONNECTION_CONNECTING,
  CONNECTION_CONNECTED,
  CONNECTION_FAILED,
};

int main(void) {
  enum ConnectionState state = CONNECTION_CONNECTED;
  printf("State selected\\n");
  return 0;
}
TERMINAL OUTPUT
Edit the model, predict the result, then run it.

This safe browser runner executes a deliberately narrow C data-model exercise. It never runs native code: it validates the lesson's enum contract and evaluates only the visible record, enum, or union state.

Try this next: Change state to CONNECTION_IDLE or CONNECTION_FAILED, run it, then reorder the enum members and observe why application code should compare names.

Unions trade independent fields for shared storage

Every structure field has its own storage. Union members instead start at the same location and share the union's storage. Write one member when the object represents one variant at a time; do not treat a union as if it holds every member independently.

union SettingValue {
  int whole_number;
  double decimal_number;
  char label[24];
};

union SettingValue value = { .decimal_number = 1.25 };

The union is sized to accommodate its largest member, with any alignment needed by the platform. That saves space when variants are exclusive, but C does not retain a built-in “active member” label for you. Code that writes decimal_number must not later pretend the same bytes are a valid label.

C DATA-MODEL RUNNER

Select exactly one union member

This runner validates the selected Metric variant. It will report the active member and reject a starter that does not establish one unambiguous interpretation.

#include <stdio.h>

union Metric {
  int whole;
  double decimal;
};

int main(void) {
  union Metric metric = { .decimal = 2.5 };
  printf("Metric selected\\n");
  return 0;
}
TERMINAL OUTPUT
Edit the model, predict the result, then run it.

This safe browser runner executes a deliberately narrow C data-model exercise. It never runs native code: it validates the lesson's union contract and evaluates only the visible record, enum, or union state.

Try this next: Change .decimal to .whole with an integer value. Then compare the outputs and explain why the object needs a tag before a larger program reads it.

Pair a union with an enum tag

A tagged union makes the active interpretation part of your program's data contract. Store a tag and union together in a structure; set both in the same construction or update function; switch on the tag before reading the matching union field.

enum SettingKind {
  SETTING_WHOLE,
  SETTING_DECIMAL,
  SETTING_LABEL,
};

struct Setting {
  enum SettingKind kind;
  union SettingValue value;
};

void print_setting(const struct Setting *setting) {
  if (setting == NULL) return;

  switch (setting->kind) {
    case SETTING_WHOLE:
      printf("%d\n", setting->value.whole_number);
      break;
    case SETTING_DECIMAL:
      printf("%.2f\n", setting->value.decimal_number);
      break;
    case SETTING_LABEL:
      printf("%s\n", setting->value.label);
      break;
    default:
      printf("invalid setting\n");
      break;
  }
}
  1. 01
    Choose the variant

    Set kind first or in the same designated initializer as the value.

  2. 02
    Write the matching member

    For SETTING_DECIMAL, assign only value.decimal_number.

  3. 03
    Read through the tag

    Every consumer switches on kind before selecting a union field.

  4. 04
    Handle impossible data

    Use default to reject or report a corrupt/unsupported tag rather than guessing.

Independent lab: build a typed configuration record

Use a local C compiler for this lesson's data-model exercises. Start with the completed type below, then add a constructor function for each variant so callers cannot accidentally set a tag that disagrees with the union member.

struct Setting make_whole_setting(int number) {
  return (struct Setting) {
    .kind = SETTING_WHOLE,
    .value.whole_number = number,
  };
}

struct Setting make_label_setting(const char label[]) {
  struct Setting setting = { .kind = SETTING_LABEL };
  /* Copy only after checking label fits setting.value.label. */
  return setting;
}
01Model a bookCreate struct Book with title, page count, and price. Initialize it with designators.
02Protect an updateWrite int restock(struct Product *product, int amount) that rejects null pointers and negative amounts.
03Name each modeAdd an enum for a task's status and write an exhaustive label function with a default fallback.
04Tag every unionAdd a Boolean variant to Setting, then update its enum, union, constructor, and printer together.

Lesson review

  • A struct groups related fields that coexist; select fields with . from an object and -> from a pointer.
  • Use a pointer parameter to mutate a caller-owned structure and const struct Type * for large read-only records.
  • An array of structures preserves each record's related fields; pass the valid count separately.
  • An enum names a limited set of meaningful states. Switch on the names, not unexplained integer values.
  • A union overlays members in shared storage and does not automatically track the valid interpretation.
  • A tagged union pairs an enum discriminator with the union and reads a member only after checking the matching tag.
KNOWLEDGE CHECK

Choose a representation deliberately

Check whether you can distinguish an object, a pointer to it, a named state, and a shared-memory variant.

01What does a struct declaration primarily model?
02How do you access the price field of a Product object named item?
03When product_pointer has type struct Product *, which expression selects its price field?
04Why is an enum useful for an order status?
05What does the default member of a C union guarantee?
06What pattern makes a union safer to use?
07If a function only needs to inspect a struct object, which parameter is typically best for a larger struct?
08What should a switch over an enum normally include while an interface is evolving?
PREVIOUS LESSONPointers and Memory Addresses
NEXT LESSONFiles and Command-Line Arguments
ON THIS PAGEStructures, Enumerations, and UnionsChoose a representationStructuresInitializationPointers and fieldsArrays of recordsEnumerationsUnionsTagged unionsIndependent labLesson reviewKnowledge check
Course contents