C Structs: Grouping Related Data

Define and use C structs to group related fields under one type, access and mutate struct members through both dot and arrow syntax, nest structs inside structs, and understand why Objective-C/Cocoa keeps core geometry types like CGPoint and CGRect as plain C structs rather than objects.

Almost every real program needs to bundle a handful of related values together — an x and y coordinate, a width and height, a name and an age. C gives you exactly one built-in tool for that: the struct. A struct is a user-defined type that packs several named fields ('members') into one contiguous block of memory, and you'll meet structs constantly once you start writing Objective-C, because Apple's own frameworks lean on them heavily — CGPoint, CGRect, CGSize, and countless others in UIKit/AppKit and Core Graphics are all plain C structs, not Objective-C objects. Understanding structs at the C level now means none of that ever feels mysterious later.

Defining a Struct and Declaring Variables

A struct definition lists a set of named members, each with its own type, inside braces: struct Point { int x; int y; };. This statement does not create any variable — it only teaches the compiler about a new type called struct Point. To actually create a variable of that type, you write struct Point p;, which allocates space for both an int x and an int y back to back in memory, uninitialized (holding garbage values) until you assign to them.

Note that in plain C, the type name is struct Point, not just Point — you must write the struct keyword every time you declare a variable of this type, function parameter, or return type. This is a common early friction point for C beginners coming from languages where a type name stands alone; the typedef technique covered later in this lesson (and in the dedicated typedef lesson) is precisely how C code sidesteps writing struct everywhere.

Struct member names live in their own namespace tied to that particular struct — two different structs can both have a member named x with no conflict, because struct Point's x and, say, struct Vector's x are accessed only through their respective struct variables.

• struct Point { int x; int y; }; declares a new type, struct Point, with two int members. • struct Point p; declares an actual variable of that type — its members start uninitialized. • In plain C you must write struct Point, not just Point, wherever the type is used. • Member names are scoped to their struct — no naming collision between different structs' fields.

Accessing and Initializing Members with the Dot Operator

Once you have a struct variable, you reach its individual members with the dot (.) operator: p.x = 3; p.y = 4; sets both fields, and printf("%d", p.x); reads one back. This is the same dot syntax you may already associate with object-oriented languages, but here it is doing something much simpler and more mechanical: it's just computing an offset into a block of memory and reading or writing the bytes at that offset.

A struct variable can also be initialized all at once at the point of declaration using brace-initialization: struct Point p = { 3, 4 }; sets x to 3 and y to 4 in the order the members were declared. Modern C also supports 'designated initializers' that name each field explicitly — struct Point p = { .x = 3, .y = 4 }; — which is more verbose but immune to bugs from reordering the struct's members later, since it no longer relies on positional order.

Structs can be assigned wholesale to one another with =: struct Point q = p; copies every member of p into a brand-new, independent struct q. This is a full value copy, exactly like copying an int — changing q.x afterward has no effect on p.x. This copy-by-value behavior is one of the most important facts about C structs, and it's the same behavior Objective-C's CGPoint, CGRect, and CGSize inherit, since they are still just C structs.

• p.x / p.y — dot operator reads or writes a member of a struct variable. • struct Point p = { 3, 4 }; — positional brace-initialization, in member declaration order. • struct Point p = { .x = 3, .y = 4 }; — designated initializer, safer against reordering. • struct Point q = p; copies every member by value — q and p are fully independent afterward.

Nested Structs

A struct member can itself be another struct type, letting you build up more complex shapes from simpler ones — exactly the pattern Apple's own frameworks use. A classic example: struct Rect { struct Point origin; struct Size size; }; where struct Size { int width; int height; }; is defined separately. This mirrors, almost field-for-field, how CGRect is actually built from CGPoint and CGSize in Apple's Core Graphics headers.

Accessing a nested member just chains dot operators: given struct Rect r;, you reach the rectangle's x-coordinate with r.origin.x. Initialization can also nest: struct Rect r = { { 0, 0 }, { 100, 50 } }; sets origin to {0, 0} and size to {100, 50} in one statement, with the inner braces matching the inner struct's own members.

Nesting structs this way keeps related data organized hierarchically instead of flattening everything into one giant struct with dozens of unrelated top-level fields — a rectangle conceptually 'has an origin' and 'has a size', and the nested struct definition expresses that relationship directly in the type itself.

• A struct member's type can be another struct, enabling composition of simpler shapes into larger ones. • Nested member access chains dots: r.origin.x. • Nested initialization uses nested braces matching the inner struct's own member list. • CGRect's actual definition mirrors this exact pattern: an origin (CGPoint) plus a size (CGSize).

Pointers to Structs and the Arrow Operator

Just like any other type, you can have a pointer to a struct: struct Point *pp = &p; stores the address of p in pp. This matters enormously in C because function parameters are passed by value — if you pass a struct by value into a function, the function receives a full copy and any changes it makes never reach the caller's original struct. Passing a pointer instead lets a function read or modify the caller's actual struct in place, which is both more flexible and, for large structs, far cheaper than copying the whole thing on every call.

Given a pointer to a struct, you could technically dereference it and then use the dot operator — (*pp).x = 5; — but this is clunky and rarely written that way in practice. C provides the arrow operator -> specifically for this: pp->x = 5; means exactly the same thing as (*pp).x = 5; but reads far more naturally. Every C programmer reaches for -> reflexively whenever they have a struct pointer rather than a struct value.

The rule of thumb is simple and worth memorizing outright: use . when you have the struct itself, and use -> when you have a pointer to the struct. This same rule carries straight over into Objective-C, where -> is occasionally used to reach an instance variable directly through self or another object pointer, bypassing accessor methods.

• struct Point *pp = &p; — a pointer variable holding the address of a struct. • (*pp).x and pp->x are exactly equivalent; -> is the idiomatic, readable form. • Passing a pointer to a struct avoids a full copy and lets a function mutate the caller's original data. • Rule of thumb: . for a struct value, -> for a pointer to a struct.

typedef with Structs and Why Cocoa Keeps Structs as Structs

Writing struct Point everywhere gets tedious fast, so C code almost universally pairs a struct definition with typedef to give it a short, bare alias: typedef struct { int x; int y; } Point; defines an anonymous struct and simultaneously names it Point, so afterward you can write Point p; with no struct keyword at all. (The typedef lesson that follows this one covers the mechanism in full generality — this is its single most common real-world application.)

This is exactly the pattern behind Apple's own CGPoint, CGRect, and CGSize: typedef struct { CGFloat x; CGFloat y; } CGPoint; and similar for the others. These types are used constantly throughout Objective-C UI code — positioning views, sizing layers, describing frames — and it is a deliberate design decision that they remain plain C structs rather than being reimplemented as Objective-C classes.

The reason is performance. Objects in Objective-C are always heap-allocated and accessed through pointers, with method calls dispatched dynamically at runtime (covered in a later module) — overhead that is entirely reasonable for a view or a data model, but wasteful for a value like 'an x/y pair' that might be created and destroyed thousands of times per second while laying out a scrollable list or animating a frame. Plain C structs, by contrast, are typically stack-allocated, copied directly, and manipulated with simple, fast, compiler-optimizable memory operations — no allocation, no reference counting, no dynamic dispatch.

This design choice does have one consequence worth flagging now and resolving later: because CGPoint, CGRect, and CGSize are not objects, they cannot be stored directly in Objective-C's object-only collections like NSArray or NSDictionary, which only hold pointers to objects. A later lesson on NSValue covers the standard solution — 'boxing' a C struct into a lightweight object wrapper specifically so it can travel through object-based APIs, and 'unboxing' it back out again when needed.

• typedef struct { int x; int y; } Point; gives a struct a short alias usable without the struct keyword. • CGPoint, CGRect, and CGSize are defined this exact way — they remain plain C structs, not objects. • This is deliberate: structs are cheap to create/copy/pass (often stack-allocated, no dynamic dispatch or ref-counting). • Consequence: structs can't go directly into NSArray/NSDictionary — NSValue (covered later) boxes them into objects when needed.

Struct Definition, Nesting, and Pointer Access

. is used on struct values; -> is used on pointers to structs. Nested initializers use nested braces that mirror the nested struct's own members. &p takes the address of p, producing the struct Point * stored in pp.

struct Point {
    int x;
    int y;
};

struct Rect {
    struct Point origin;
    struct Point corner;
};

struct Point p = { 3, 4 };
struct Rect r = { { 0, 0 }, { 10, 10 } };

struct Point *pp = &p;
pp->x = 99;          // same as (*pp).x = 99;
int cornerX = r.corner.x;

Code Examples

Assigning q = p copies every member of p into q; mutating q.x afterward has zero effect on the original p, demonstrating that struct assignment in C is always a full, independent value copy. Expected result: p = (3, 4) p.x is still 3, q.x is now 100 movePoint receives the address of p, not a copy, so writes through pp->x and pp->y modify the caller's original struct directly — this is the only way for a C function to mutate a struct it didn't create itself. Expected result: p = (5, 10)

This mirrors, member for member, how Apple's own CGPoint/CGSize/CGRect are defined — plain typedef'd C structs composed of other typedef'd C structs, with no Objective-C class or dynamic dispatch involved anywhere. In real Objective-C/Cocoa code you'd use the built-in CGPoint/CGSize/CGRect and CGRectGetMaxX(r) rather than redefining your own — this example exists purely to show the underlying mechanism.

#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main(void) {
    struct Point p = { 3, 4 };
    printf("p = (%d, %d)\n", p.x, p.y);

    struct Point q = p;   // full value copy
    q.x = 100;
    printf("p.x is still %d, q.x is now %d\n", p.x, q.x);
    return 0;
}
#include <stdio.h>

struct Point { int x; int y; };

void movePoint(struct Point *pp, int dx, int dy) {
    pp->x += dx;   // equivalent to (*pp).x += dx;
    pp->y += dy;
}

int main(void) {
    struct Point p = { 0, 0 };
    movePoint(&p, 5, 10);
    printf("p = (%d, %d)\n", p.x, p.y);
    return 0;
}
typedef struct {
    double x;
    double y;
} Point;

typedef struct {
    double width;
    double height;
} Size;

typedef struct {
    Point origin;
    Size size;
} Rect;

Rect r = { { 10.0, 20.0 }, { 320.0, 480.0 } };
double rightEdge = r.origin.x + r.size.width;

Struct Value vs Struct Pointer

Use the dot operator on the struct itself; use the arrow operator when you're holding a pointer to it.

struct Point p = {3, 4};      struct Point *pp = &p;

  Memory:                        pp stores an address:
  ┌──────┬──────┐                ┌───────────┐
  │ x: 3 │ y: 4 │  <─────────────│  address  │
  └──────┴──────┘                └───────────┘
     p.x   p.y                    pp->x  pp->y
   (direct access)          (dereference-then-access)

Important Rules

Always write struct Name for the type in plain C, unless a typedef has given it a bare alias.

Use . to access members of a struct value; use -> to access members through a struct pointer.

Remember struct assignment (=) is a full value copy, not a reference — mutating a copy never affects the original.

Pass large structs by pointer (struct Point *) to functions that need to modify them or avoid an expensive copy.

Prefer designated initializers ({ .x = 1, .y = 2 }) over purely positional ones once a struct has more than two or three members.

Structs Underneath Every View You Lay Out

Every time an Objective-C app positions a button, sizes a label, or animates a layer's frame, it is reading and writing plain C structs — CGPoint, CGSize, CGRect — millions of times over an app's lifetime. Keeping them as lightweight, stack-allocated C structs rather than heap-allocated objects is a deliberate performance decision that keeps UI layout and animation fast even on constrained mobile hardware.

struct Point p; p->x = 3; // p is a value, not a pointer struct Point p; p.x = 3;

The arrow operator is only valid on a pointer to a struct; using it on a plain struct value is a compile-time type error. Use the dot operator for struct values.

void reset(struct Point p) { p.x = 0; p.y = 0; } // caller's struct is unchanged after calling reset(myPoint) void reset(struct Point *pp) { pp->x = 0; pp->y = 0; } // call as reset(&myPoint);

Passing a struct by value gives the function its own independent copy; any writes inside the function are invisible to the caller. Pass a pointer when the function needs to modify the caller's struct.

Given struct Point p = {1, 2}; and struct Point *pp = &p;, which expression correctly sets p's x member to 10 through the pointer? A. p.x = 10; B. pp.x = 10; C. pp->x = 10; D. *p->x = 10;

C. pp->x = 10; — pp is a pointer to a struct, so the arrow operator is required: pp->x = 10; is equivalent to (*pp).x = 10;. Plain dot access (pp.x) is invalid on a pointer.

After struct Point q = p; followed by q.x = 99;, what happens to p.x? A. p.x also becomes 99, since q and p share memory B. p.x is unchanged, because struct assignment copies by value C. It causes a compile error D. p.x becomes 0

B. p.x is unchanged, because struct assignment copies by value — Struct assignment in C copies every member into a fully independent new struct; q and p do not share storage, so modifying q afterward never affects p.

Why do Apple's Core Graphics types like CGPoint and CGRect remain plain C structs instead of Objective-C classes? A. Objective-C cannot define classes with numeric fields B. Structs are typically cheaper to create, copy, and pass than heap-allocated objects with dynamic dispatch — important for values used constantly in UI layout and animation C. It's a historical accident with no real benefit today D. NSArray requires its contents to be structs, not objects

B. Structs are typically cheaper to create, copy, and pass than heap-allocated objects with dynamic dispatch — important for values used constantly in UI layout and animation — Structs avoid heap allocation, reference counting, and dynamic method dispatch, making them far cheaper for high-frequency values like coordinates and sizes — a deliberate performance-driven design choice.

What does r.origin.x mean, given struct Rect { struct Point origin; struct Point corner; };? A. It's invalid syntax — you can't chain dot operators B. It accesses the x member of the origin member, which is itself a nested struct C. It multiplies r.origin by x D. It only works if origin is a pointer

B. It accesses the x member of the origin member, which is itself a nested struct — Nested struct members are reached by chaining the dot operator: r.origin is a struct Point, and .x reaches that inner struct's own x member.

Define a struct Circle with a struct Point center and a double radius. Write a function void scaleCircle(struct Circle *c, double factor) that multiplies the circle's radius by factor through the pointer, then use it in main() on a circle centered at (0, 0) with radius 5, scaling it by 2.0, and print the new radius.

Nest struct Point inside struct Circle, and remember to use -> since scaleCircle receives a pointer.