C / STRUCTS, UNIONS AND ENUMS
Passing structs by value versus by pointer
Decide when a function should take a struct by value and when it should take a pointer, and predict exactly which writes the caller will see.
What you will learn
- Predict from a parameter type whether a function can modify the caller's struct
- Pass small structs by value; pass large read-only ones as const struct T *
- Use an out-pointer when a function must write results into the caller's object
- Know that an array inside a struct is copied on pass, unlike a bare array param
Understanding Passing structs by value versus by pointer
C passes every argument by value, and a struct is no exception: the parameter is a brand-new object whose members are copied from the argument, with its own address and a lifetime that ends when the function returns. Writing s.count = 3 inside such a function edits that clone and nothing else, which is why the caller sees no change at all. Passing "by pointer" is not a second mechanism, it is the same one applied to an address: you copy the pointer, and the copy still points at the caller's object.
The copy is member for member and includes nested structs and arrays, so its cost grows with sizeof the type. A struct of two ints usually arrives in registers, while a 400-byte struct means a real block copy on every call. That is the reasoning behind the usual idiom: small read-only structs go by value, large ones go as const struct T *, and anything the function must write to goes as struct T *. The const carries real information, because a plain pointer parameter tells the reader nothing about whether the object will be modified, while a value parameter promises it cannot be.
The mental model is a choice between a snapshot and a handle. A snapshot is isolated: nothing that happens elsewhere during the call can change it, and the parameter doubles as free scratch space you may overwrite at will. A handle carries identity, so you can write back to the original, compare addresses, chain nodes together, and skip the copy. In exchange the callee inherits two questions a value parameter never raises: can this pointer be NULL, and does the object it names still exist?
<stdio.h>
<string.h>
struct Sensor {
int id;
double reading;
char label[8];
};
static void show(const char *when, const struct Sensor *s)
{
printf("%s: %.1f %s\n", when, s->reading, s->label);
}
/* s is a private clone of the caller's struct, label array included. */
static void tweak_copy(struct Sensor s)
{
s.reading = 99.0;
strcpy(s.label, "copy");
show("inside tweak_copy", &s);
}
/* s holds the address of the caller's struct, so the writes land there. */
static void tweak_target(struct Sensor *s)
{
s->reading = 99.0;
strcpy(s->label, "ptr");
show("inside tweak_target", s);
}
int main(void)
{
struct Sensor probe = { 7, 20.5, "raw" };
show("before", &probe);
tweak_copy(probe);
show("after tweak_copy", &probe);
tweak_target(&probe);
show("after tweak_target", &probe);
return 0;
}
A struct argument copies the whole object, while a pointer argument copies only an address, so only the pointer form can reach the caller's struct.
Worked examples
Return a new value, or write through an out-pointer
The same computation exposed twice: once returning a fresh struct by value, once mutating the caller's struct through a pointer.
<stdio.h>
struct Vec2 { double x, y; };
/* Returns a fresh value; the caller's struct cannot be touched. */
static struct Vec2 scaled(struct Vec2 v, double k)
{
v.x *= k;
v.y *= k;
return v;
}
/* Writes through an out-pointer; no struct is copied on the way in. */
static void scale_in_place(struct Vec2 *v, double k)
{
v->x *= k;
v->y *= k;
}
int main(void)
{
struct Vec2 a = { 1.5, -2.0 };
struct Vec2 b = scaled(a, 2.0);
printf("a = (%.1f, %.1f)\n", a.x, a.y);
printf("b = (%.1f, %.1f)\n", b.x, b.y);
scale_in_place(&a, 2.0);
printf("a = (%.1f, %.1f)\n", a.x, a.y);
struct Vec2 c = a;
scale_in_place(&a, 10.0);
printf("c = (%.1f, %.1f)\n", c.x, c.y);
return 0;
}
Example explained
Line 1scaled assigns to v, which is already a copy, so a at the call site is untouched after the call.
Line 2return v; copies the parameter back out to the caller, which is how b receives the scaled numbers without any pointer being involved.
Line 3scale_in_place(&a, 2.0) hands over an address instead of two doubles, so the multiplications land in a itself.
Line 4struct Vec2 c = a; copies every member, so the later write through &a cannot reach c.
An array member is copied, a bare array parameter is not
Shows that wrapping an array in a struct changes it from pass-by-address to pass-by-copy.
<stdio.h>
struct Buf { char text[6]; };
static void via_array(char text[6]) /* adjusted to char *: no copy */
{
text[0] = 'X';
}
static void via_struct(struct Buf b) /* the array inside is copied */
{
b.text[0] = 'X';
}
int main(void)
{
char raw[6] = "abcde";
struct Buf wrapped = { "abcde" };
via_array(raw);
via_struct(wrapped);
printf("raw = %s\n", raw);
printf("wrapped = %s\n", wrapped.text);
return 0;
}
Example explained
Line 1The parameter char text[6] is adjusted to char *, so via_array receives the address of raw and text[0] = 'X' edits the caller's array.
Line 2struct Buf b is a genuine copy, so the identical-looking b.text[0] = 'X' edits the clone and wrapped still reads abcde.
Line 3This is why wrapping an array in a struct is the standard trick when you want an array that can be assigned, passed, and returned as one value.
Important notes
By value is not automatically slower. A two-int struct usually travels in registers while a pointer forces the callee to read through memory; copies only start to hurt for large structs or calls inside hot loops.
const struct T *p forbids writes through p only. The object can still change through another pointer or in the caller, so const is a promise about that one parameter, not a guarantee of immutability.
Common mistakes
Assigning to members of a by-value parameter and expecting the caller to see it: the code compiles and runs while quietly discarding every write, so the bug surfaces later as stale data.
Calling f(probe) when f takes struct Sensor *: the compiler reports incompatible types, and forcing it through with a cast makes the function treat the struct's first bytes as an address and crash.
Returning &s where s is a by-value parameter or a local struct: that object dies at the closing brace, so the caller holds a dangling pointer that seems to work until something else reuses the stack.
Try it yourself
Change, predict, then run
Define struct Rect { double w, h; }; then write void grow(struct Rect *r, double k) and struct Rect grown(struct Rect r, double k). Call each one on the same rectangle and print w and h after each call so you can see which call changed the original.
Open the C workspaceCheck your understanding
A function is written as void f(struct P a, struct P *b) { b->x = 9; printf("%d\n", a.x); } and the caller does struct P p = {1}; f(p, &p); What is printed?
- 1, because a was copied before the body ran and is a separate object
- 9, because a and b both refer to the same struct p
- Nothing defined: passing a struct and a pointer to it in the same call is undefined behaviour
- 1 or 9 depending on how the compiler orders the argument copies
Show answer
The copy into a happens as part of making the call, so a is a snapshot of p taken before the body executes, and b->x = 9 changes p itself without being able to reach that snapshot. Option 2 is tempting because both arguments came from p, but only b is a view of p; a is a distinct object with its own storage. Argument order is irrelevant here because all copying finishes before the first statement runs, and the call itself is perfectly well defined.