Structs and strings in C — theory with cinema seats, fields in memory and the ‘\0’ nobody sees (and the long name that changed the ID of the person next door)
In the IC2 lab we were given a program with a struct person that stored the ID number and the name of each viewer in a cinema. To check that it worked, I swapped “Pepito” for a longer name: “Maximiliano Ruiz”. It compiled without a single warning. I ran it and the name came out perfect… but the viewer in the seat next door had changed ID number and lost their name. I hadn’t touched that seat. This is the program (cut down to the essentials):
#include <stdio.h>
#include <string.h>
// Same as in the class lab: an ID number and a 10-character name
struct person {
int id;
char name[10];
};
int main(void) {
// Two seats in a row: in memory, one right after the other
struct person row[2] = {{42345678, "Pepito"}, {12343278, "Juan"}};
printf("Before:\n");
for (int i = 0; i < 2; i++)
printf(" seat %d: %d %s\n", i, row[i].id, row[i].name);
// "Maximiliano Ruiz" is 16 characters + '\0' = 17 bytes... in a 10-byte slot
char new_name[] = "Maximiliano Ruiz";
strcpy(row[0].name, new_name);
printf("After copying \"%s\" into seat 0:\n", new_name);
for (int i = 0; i < 2; i++)
printf(" seat %d: %d %s\n", i, row[i].id, row[i].name);
return 0;
}
And this is what came out on screen:
Before: seat 0: 42345678 Pepito seat 1: 12343278 Juan After copying "Maximiliano Ruiz" into seat 0: seat 0: 42345678 Maximiliano Ruiz seat 1: 2053731666
How can a strcpy on row[0] change row[1]? It’s neither magic nor a compiler bug: a struct is a block of bytes with its fields one after another, a string in C is a char array that ends in an invisible character, '\0', and strcpy copies up to that character without checking whether it fits. In this article you’ll understand all three, and at the end of the memory section you’ll see byte by byte what Maximiliano overwrote.
We’re coming from dynamic memory in C, where we were already reserving arrays on the heap. Now it’s time to give them shape: instead of an array of int, an array of seats with several pieces of data each.
Table of Contents
What a struct is and what it’s for
Until now, if you wanted to store a seat’s data (row, number, viewer and price) you needed four separate variables, or four parallel arrays if there were lots of seats. It works, but it’s fragile: sort one array and forget the other three, and the names no longer match their seats.
A struct (structure) groups several pieces of data of different types under a single name. It’s the closest thing C has to an object, but without methods: just fields.
#include <stdio.h>
#include <string.h>
// The template: it says which fields a seat has, but it does not reserve memory yet
struct seat {
char row; // row letter: 'A', 'B'...
int number; // seat number within the row
char name[20]; // viewer: 19 characters at most + '\0'
double price;
};
// Receives the seat BY VALUE (a copy): it only reads it
void show(struct seat b) {
printf("%c%-3d %-8s %5.2f euros\n", b.row, b.number, b.name, b.price);
}
int main(void) {
// 1) Initialisation in the order of the fields
struct seat a = {'C', 7, "Lucia", 8.50};
// 2) Designated initialisers (C99): by name and in any order; whatever is missing is 0
struct seat b = {.number = 8, .row = 'C', .price = 8.50};
// 3) Field by field, with the dot operator
struct seat c;
c.row = 'F';
c.number = 12;
c.price = 6.00;
// name is an array: c.name = "Ivan" does not compile; it is copied with strcpy
strcpy(c.name, "Ivan");
show(a);
// b.name was left at 0, and 0 is exactly '\0': an empty string
show(b);
show(c);
// A struct CAN be assigned whole: it copies all its bytes, array included
struct seat d = a;
d.number = 9;
strcpy(d.name, "Marcos");
printf("After copying a into d and changing d:\n");
show(a);
show(d);
printf("sizeof(struct seat) = %zu bytes\n", sizeof(struct seat));
return 0;
}
Before looking at the output, notice three things in the code:
struct seat { ... };is the template: it says which fields there are and of which type, but it doesn’t take up memory yet. Memory appears when you declare a variable:struct seat a;. Watch the;after the brace: forgetting it causes strange errors on the next line.- The type is called
struct seat, both words. Writing justseat a;doesn’t compile (further down you’ll see how to shorten it withtypedef). - You reach each field with the dot operator:
c.row,c.number,c.name.
When you run it:
C7 Lucia 8.50 euros C8 8.50 euros F12 Ivan 6.00 euros After copying a into d and changing d: C7 Lucia 8.50 euros C9 Marcos 8.50 euros sizeof(struct seat) = 40 bytes
The three ways to give it values
| Way | Example | When to use it |
|---|---|---|
| In order | {'C', 7, "Lucia", 8.50} | Small structs you know by heart |
| Designated (C99) | {.number = 8, .row = 'C', .price = 8.50} | When there are many fields or you want to leave some at 0 |
| Field by field | c.row = 'F'; | When the data arrives later (from a file, the keyboard…) |
In seat b I didn’t give a name, and the output shows an empty gap. Why not garbage? Because in a brace initialisation everything you don’t name is 0, and byte 0 is precisely the null character '\0': an empty string.
A struct is copied whole; an array is not
Look at the line struct seat d = a;. It copies the 40 bytes of a into d, **name array included**. Then we change d and a stays the same (the last two lines of the output prove it: C7 Lucia and C9 Marcos). They’re two independent seats.
And here comes the paradox that confuses everybody: if a struct can be assigned with =, why doesn’t c.name = "Ivan"; compile? Because c.name is an array, and in C an array can’t be assigned after it’s declared. To put text into it you have to copy it character by character, which is what strcpy(c.name, "Ivan") does.
viewer.name = "Pepito"; gives error: assignment to expression with array type. The struct isn’t wrong: arrays just can’t be assigned. Use strcpy (or snprintf, which you’ll see below).
With typedef struct seat { ... } Seat; you can write Seat a; instead of struct seat a;. In the class lab it appears like this: typedef struct person { ... } Persona;. It’s only an alias; inside it’s the same thing.
And that sizeof(struct seat) = 40? The fields add up to 1 + 4 + 20 + 8 = 33 bytes. The missing 7 are the key to the next section.
Fields in memory: offsets and padding
The IC2 lecture notes explain it with an example: if a struct with an int n1, a float n2 and a char st[10] starts at position 100, then n1 is at 100, n2 at 104 and st at 108. Fields go one after another, in the order you write them. Let’s check it with offsetof, which tells you how many bytes from the start each field begins:
#include <stdio.h>
#include <stddef.h> // offsetof: how many bytes from the start each field begins
// The example from the lecture notes: int, float and char[10]
struct data {
int n1;
float n2;
char st[10];
};
// The one from the hook seat
struct person {
int id;
char name[10];
};
// The same three fields in two different orders
struct unordered { char a; int b; char c; };
struct ordered { int b; char a; char c; };
int main(void) {
struct data d1;
// The lecture notes assume d1 starts at position 100
printf("struct data (d1 at position 100):\n");
printf(" n1 -> offset %2zu -> position %zu\n", offsetof(struct data, n1), 100 + offsetof(struct data, n1));
printf(" n2 -> offset %2zu -> position %zu\n", offsetof(struct data, n2), 100 + offsetof(struct data, n2));
printf(" st -> offset %2zu -> position %zu\n", offsetof(struct data, st), 100 + offsetof(struct data, st));
printf(" sizeof = %zu (the fields add up to 4 + 4 + 10 = 18)\n", sizeof d1);
// With real addresses: they change on every run, but the distance between fields does not
printf(" &d1.st - &d1.n1 = %td bytes\n", (char *)&d1.st - (char *)&d1.n1);
printf("struct person: id at %zu, name at %zu, sizeof = %zu\n",
offsetof(struct person, id), offsetof(struct person, name), sizeof(struct person));
printf("unordered: a at %zu, b at %zu, c at %zu, sizeof = %zu\n",
offsetof(struct unordered, a), offsetof(struct unordered, b),
offsetof(struct unordered, c), sizeof(struct unordered));
printf("ordered: b at %zu, a at %zu, c at %zu, sizeof = %zu\n",
offsetof(struct ordered, b), offsetof(struct ordered, a),
offsetof(struct ordered, c), sizeof(struct ordered));
return 0;
}
The output:
struct data (d1 at position 100): n1 -> offset 0 -> position 100 n2 -> offset 4 -> position 104 st -> offset 8 -> position 108 sizeof = 20 (the fields add up to 4 + 4 + 10 = 18) &d1.st - &d1.n1 = 8 bytes struct person: id at 0, name at 4, sizeof = 16 unordered: a at 0, b at 4, c at 8, sizeof = 12 ordered: b at 0, a at 4, c at 5, sizeof = 8
The positions match the notes: 100 + 0 = 100, 100 + 4 = 104 and 100 + 8 = 108. But sizeof is 20, not 18. Where do those 2 bytes come from?
From padding. The processor reads an int or a float faster if its address is a multiple of 4, so the compiler aligns each field and rounds the total size of the struct up to a multiple of 4. 18 isn’t a multiple of 4; the next one is 20. Those 2 padding bytes have no name and you don’t use them, but they’re there.
You can see it even more clearly in the last two lines. The same fields (char, int, char) take up 12 bytes in one order and 8 in another:
| Struct | Fields and position | Sum | sizeof |
|---|---|---|---|
unordered | a at 0, (3 padding), b at 4, c at 8, (3 padding) | 1 + 3 + 4 + 1 + 3 | 12 |
ordered | b at 0, a at 4, c at 5, (2 padding) | 4 + 1 + 1 + 2 | 8 |
person | id at 0, name at 4, (2 padding) | 4 + 10 + 2 | 16 |
Padding depends on the compiler and the machine. That’s why you should never work out the size of a struct by adding up fields by hand: always use sizeof(struct ...), as we did in dynamic memory with malloc(n * sizeof *seats).
What Maximiliano overwrote
Now we have everything we need to explain the hook. struct person takes up 16 bytes: 4 for id, 10 for name and 2 of padding. An array of two people is 32 bytes in a row: the second one starts exactly where the first one ends. Look at the drawing:
“Maximiliano Ruiz” is 16 characters plus the '\0': 17 bytes. strcpy starts writing at row[0].name (byte 4) and doesn’t stop until it has copied the null:
- Bytes 4 to 13: “Maximilian” fills the 10 bytes of
name. - Bytes 14 and 15: “o” and the space land in the padding. Nobody notices.
- Bytes 16 to 19: “Ruiz” lands in the
idofrow[1]. Those four bytes (R = 82, u = 117, i = 105, z = 122) read as anintgive 2053731666. - Byte 20: the
'\0'lands on the first character ofrow[1].name. Its name is now an empty string.
And why does row[0].name print the full name? Because printf("%s") reads from byte 4 until it finds a '\0', which is now at byte 20. The original “Juan” is still in memory (the drawing shows “uan” after the null), but nobody reaches it any more.
Thinking that if there’s no Segmentation fault, there’s no bug. Writing outside a field is undefined behaviour: sometimes it crashes, sometimes it silently ruins a neighbouring piece of data, like here. The compiler didn’t even warn.
Dot versus arrow
You’ll almost always work with pointers to structs: to pass them to functions without copying them and to store them on the heap. That’s where the arrow -> comes in.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct person {
int id;
char name[10];
};
// By value: the function works with a COPY and the change is lost on return
void rename_copy(struct person p, const char *new_name) {
strcpy(p.name, new_name);
}
// By pointer: it works with the ORIGINAL through its address
void rename(struct person *p, const char *new_name) {
// p->name is exactly the same as (*p).name
strcpy(p->name, new_name);
}
int main(void) {
struct person viewer = {42345678, "Pepito"};
struct person *p = &viewer;
// Three ways to reach the same field
printf("viewer.id = %d\n", viewer.id);
printf("(*p).id = %d\n", (*p).id);
printf("p->id = %d\n", p->id);
rename_copy(viewer, "Luis");
printf("After rename_copy: %s\n", viewer.name);
rename(&viewer, "Luis");
printf("After rename: %s\n", viewer.name);
// With dynamic memory (the previous article): 3 seats on the heap
int n = 3;
struct person *seats = malloc(n * sizeof *seats);
if (seats == NULL)
return 1;
int ids[] = {11111111, 22222222, 33333333};
char names[3][10] = {"Ana", "Bruno", "Carla"};
for (int i = 0; i < n; i++) {
// seats[i] is already a struct: we use the dot
seats[i].id = ids[i];
// seats + i is a pointer: we use the arrow
strcpy((seats + i)->name, names[i]);
}
// q walks the array struct by struct: q++ moves sizeof(struct person) bytes
for (struct person *q = seats; q < seats + n; q++)
printf(" %d %s\n", q->id, q->name);
free(seats);
return 0;
}
When you run it:
viewer.id = 42345678 (*p).id = 42345678 p->id = 42345678 After rename_copy: Pepito After rename: Luis 11111111 Ana 22222222 Bruno 33333333 Carla
The first three lines print the same thing because they reach the same int:
viewer.id: you have the struct, you use the dot.(*p).id: you have a pointer;*pis the struct it points to, and you apply the dot to that struct. The parentheses are compulsory:*p.idmeans*(p.id), because the dot binds tighter than the asterisk, and gcc complains with'p' is a pointer; did you mean to use '->'?.p->id: it’s exactly(*p).id, written more comfortably. The arrow means “follow the pointer and go into the field”.
| What I have | Example | To reach a field |
|---|---|---|
| A struct | struct person viewer | viewer.id |
| A pointer to a struct | struct person *p | p->id |
| A slot in an array of structs | seats[i] | seats[i].id (it’s a struct) |
| An address inside the array | seats + i | (seats + i)->id |
By value or by pointer
rename_copy receives the struct by value: C copies the 16 bytes into the parameter p and the function changes that copy. On return, the copy disappears and viewer is still called Pepito (the fourth line of the output says so). rename receives the address, so p->name is the original’s name and the change sticks: Luis.
It’s the same thing you saw in functions in C with pass by reference, but it shows more with structs: besides not losing the changes, you save copying the whole struct on every call.
If a function only reads the struct but you don’t want to copy it, pass it as const struct person *p. That way the compiler warns you if you try to modify it by mistake.
Structs on the heap
The last part of the program links to the previous article: malloc(n * sizeof *seats) reserves 3 × 16 = 48 bytes for three people. Notice the two ways in: seats[i] is already a struct (dot) and seats + i is an address (arrow). And the last loop walks the array with a pointer: each q++ moves 16 bytes, a whole struct, not one byte. Pointer arithmetic from pointers in C works the same way with structs.
Strings in C: char arrays with a ‘\0’ at the end
C has no string type. A string is, by convention, a char array whose end is marked with the null character '\0' (byte 0). All string functions (printf("%s"), strlen, strcpy…) walk through characters until they find that 0. They don’t know how big the array is: they only know where the null is.
#include <stdio.h>
#include <string.h>
// Counts characters until it finds the '\0', like the version in the lecture notes
unsigned int length(const char *s) {
unsigned int n = 0;
// The loop stops at the null character, which is 0 (false)
while (*s != '\0') {
n++;
// Moves the pointer to the next char
s++;
}
return n;
}
int main(void) {
// The compiler reserves 4 bytes: 'A' 'n' 'a' '\0'
char name[] = "Ana";
// 10 bytes reserved: the spare ones are filled with '\0'
char gap[10] = "Ana";
printf("sizeof(name) = %zu, strlen(name) = %zu\n", sizeof name, strlen(name));
printf("sizeof(gap) = %zu, strlen(gap) = %zu\n", sizeof gap, strlen(gap));
printf("length(\"Pepito\") = %u\n", length("Pepito"));
// Byte by byte, null included, with its numeric code
for (size_t i = 0; i < sizeof name; i++) {
if (name[i] == '\0')
printf("name[%zu] = '\\0' (%d)\n", i, name[i]);
else
printf("name[%zu] = '%c' (%d)\n", i, name[i], name[i]);
}
// '0' (the digit) and '\0' (the null) have nothing to do with each other
printf("'0' is %d and '\\0' is %d\n", '0', '\0');
// char[] is our own copy and can be modified
char room[] = "room 1";
room[5] = '3';
// char* to a literal points to read-only memory: film[0] = 'K' would crash the program
const char *film = "Coco";
printf("%s / %s\n", room, film);
return 0;
}
The output:
sizeof(name) = 4, strlen(name) = 3
sizeof(gap) = 10, strlen(gap) = 3
length("Pepito") = 6
name[0] = 'A' (65)
name[1] = 'n' (110)
name[2] = 'a' (97)
name[3] = '\0' (0)
'0' is 48 and '\0' is 0
room 3 / Coco
Let’s go line by line with this drawing in front of us:
char name[] = "Ana";reserves 4 bytes, not 3: the compiler adds the'\0'on its own. That’s whysizeof(name)gives 4 andstrlen(name)gives 3.char gap[10] = "Ana";reserves 10 bytes; the 7 spare slots are filled with'\0'.sizeofstill measures the whole array (10) andstrlenstops at the first null (3).length("Pepito")counts 6: the loopwhile (*s != '\0')moves the pointer letter by letter and stops at the null, which doesn’t count.- The byte-by-byte walk shows the codes: ‘A’ is 65, ‘n’ is 110, ‘a’ is 97 and the null is 0.
| Expression | What it measures | “Ana” in char gap[10] |
|---|---|---|
sizeof gap | Bytes reserved for the array (the compiler knows it) | 10 |
strlen(gap) | Characters up to the first '\0' (counted at run time) | 3 |
| What fits | sizeof - 1 characters, because the null takes up space too | 9 |
That’s the golden rule: a char name[10] holds 9 letters at most. The tenth slot is for the '\0'. In the hook, “Maximiliano Ruiz” needed 17.
Confusing '0' with '\0'. The output makes it clear: the digit '0' is 48 and the null is 0. Writing name[3] = '0'; doesn’t end the string: it adds a real zero.
char[] versus char*
The last two lines of the program hide another important difference:
char room[] = "room 1";creates your own array with a copy of the text. You can change its letters:room[5] = '3'gives “room 3”.const char *film = "Coco";creates a pointer to a literal that the compiler keeps in read-only memory. Tryingfilm[0] = 'K'compiles (if you remove theconst), but the program dies withSegmentation fault. That’s why I addedconst: so the compiler stops it first.
Declare as const char * every pointer that points to a literal or that a function will only read. The string.h functions do it: strlen(const char *s).
string.h: copy, join, compare and search
Since you can’t assign arrays or compare them with ==, C comes with a whole library for strings: #include <string.h>. These are the ones you’ll use most in IC2:
#include <stdio.h>
#include <string.h>
int main(void) {
char name[20];
char greeting[40] = "Hello, ";
// strcpy(dest, src): copies up to and including the '\0'. It does NOT check if it fits
strcpy(name, "Lucia");
// strcat(dest, src): appends at the end, starting where dest's '\0' was
strcat(greeting, name);
strcat(greeting, "!");
printf("%s\n", greeting);
// strcmp: 0 if they are equal; negative if the first goes before; positive if it goes after
char a[] = "Ana", b[] = "Bruno", c[] = "ana";
printf("strcmp(Ana, Ana) = %d\n", strcmp(a, "Ana"));
printf("strcmp(Ana, Bruno) = %d\n", strcmp(a, b));
printf("strcmp(ana, Ana) = %d\n", strcmp(c, a));
// strncpy copies at most n bytes... and if it does not fit, it does NOT add the '\0'
char short_label[6];
strncpy(short_label, "Maximiliano", sizeof short_label - 1);
// That is why we put it in the last slot ourselves
short_label[sizeof short_label - 1] = '\0';
printf("strncpy truncated: %s\n", short_label);
// snprintf never writes more than n bytes and always ends in '\0'
char label[12];
int needed = snprintf(label, sizeof label, "%c%d-%s", 'F', 7, "Maximiliano");
printf("snprintf: %s (needed %d characters)\n", label, needed);
// strchr looks for a character and returns a pointer to it (or NULL if it is not there)
const char *record = "42345678;Pepito";
const char *pc = strchr(record, ';');
printf("';' at position %td, name = %s\n", pc - record, pc + 1);
return 0;
}
Before running it, the compiler already says something:
stringh.c: In function 'main': stringh.c:30:54: warning: '%s' directive output truncated writing 11 bytes into a region of size 9 [-Wformat-truncation=] stringh.c:30:18: note: 'snprintf' output 15 bytes into a destination of size 12
It’s a warning, not an error: gcc has seen that “F7-Maximiliano” doesn’t fit in 12 bytes and that snprintf is going to cut it. Here we do it on purpose, but it’s a warning worth always reading. The output:
Hello, Lucia! strcmp(Ana, Ana) = 0 strcmp(Ana, Bruno) = -1 strcmp(ana, Ana) = 32 strncpy truncated: Maxim snprintf: F7-Maximili (needed 14 characters) ';' at position 8, name = Pepito
| Function | What it does | Watch out for |
|---|---|---|
strlen(s) | Counts characters up to the '\0' | Doesn’t count the null; walks the whole string every time |
strcpy(dst, src) | Copies src into dst, null included | Doesn’t check that it fits: that’s what happened in the hook |
strcat(dst, src) | Appends src to the end of dst | dst must have room for both and the null |
strcmp(a, b) | Compares letter by letter: 0 if equal | Only the sign matters, not the number |
strncpy(dst, src, n) | Copies at most n bytes | If it doesn’t fit, it **doesn’t add the '\0'** |
snprintf(dst, n, fmt, ...) | Writes with a format, at most n bytes | Always ends in '\0'; returns what it would have needed |
strchr(s, c) | Pointer to the first c in s, or NULL | You have to check for NULL before using it |
Let’s go through what the output says:
- strcpy and strcat:
greetingstarted as “Hello, ” and ended up as “Hello, Lucia!”.strcatstarts writing exactly where the'\0'ofgreetingwas and adds a new one at the end. It works becausegreetinghas 40 bytes and we only use 14. - strcmp: “Ana” with “Ana” gives 0 (equal). “Ana” with “Bruno” gives a negative number: A goes before B. “ana” with “Ana” gives a positive 32: lowercase ‘a’ (97) goes after uppercase ‘A’ (65), and 97 − 65 = 32. For C, uppercase and lowercase are different letters.
- strncpy: it copies 5 letters of “Maximiliano” and we put the null in slot 5: “Maxim”. Without that line,
short_labelwouldn’t be a string. - snprintf: of the 14 characters “F7-Maximiliano” needed, only 11 plus the null fit: “F7-Maximili”. And it returns 14, so you can detect that it cut something (
needed >= sizeof label). - strchr: the
';'is at position 8 (8 ID digits, from 0 to 7). Subtracting two pointers gives the distance:pc - record = 8. Andpc + 1points to the first letter of the name, so printing it gives “Pepito” without copying anything.
The standard only guarantees the sign of strcmp. With Fedora’s library, “Ana” against “Bruno” gave −1, but “ana” against “Ana” gave 32. Always compare with == 0, < 0 or > 0, never with == -1.
Why you don’t compare with ==
If you write if (name == "Ana"), C doesn’t compare letters: it compares addresses. Is the array name at the same address as the literal “Ana”? Almost never, so the condition is false even if the text is identical. gcc warns about it with comparison with string literal results in unspecified behavior. You’ll see it fail live in the exercises. The right way is strcmp(name, "Ana") == 0.
When do I use each thing?
| Situation | What to use | Why |
|---|---|---|
| Several pieces of data that go together (a seat, a student) | struct | One name, passed and copied in one go |
| A function that changes the struct | struct x *p and p->field | By value you’d change a copy |
| A function that only reads it | const struct x *p | No copy, and the compiler protects the original |
| Text with a known maximum size | char name[N] inside the struct | Simple; remember N − 1 letters fit |
| Text whose size varies a lot | char * + malloc(strlen(s) + 1) | Each text takes exactly what it needs (you’ll do it in the practice) |
| Copying text that might not fit | snprintf(dst, sizeof dst, "%s", src) | Never goes over and always adds the '\0' |
| Comparing text | strcmp(a, b) == 0 | == compares addresses |
Common mistakes
Forgetting the ; after the closing brace of struct ... { };. The error appears on the next line and throws you off.
Reserving the array just for the letters: char id[8] for an 8-digit ID number. You need char id[9]. Otherwise the '\0' spills out, as in the hook.
Using strncpy as if it were safe. If the source is longer than n, the destination is left without a null and the next printf keeps reading someone else’s memory. Add the '\0' yourself or use snprintf.
Writing p.id with a pointer or viewer->id with a struct. gcc says it very clearly: 'p' is a pointer; did you mean to use '->'? in the first case and invalid type argument of '->' (have 'struct person') in the second. Ask yourself: do I have the struct or its address?
If you suspect you’re going out of an array, compile with gcc -Wall -std=c11 -g -fsanitize=address program.c. AddressSanitizer stops the program at the exact line of the overflow. Careful: inside a single struct (as in the hook) it doesn’t always catch it, because for it that’s still the struct’s memory.
Watch it step by step in Python Tutor
Python Tutor isn’t only for Python: choose C in the language drop-down and you’ll see the stack with each variable, structs as boxes with their fields and pointers as arrows. Paste this program:
#include <stdio.h>
#include <string.h>
struct person {
int id;
char name[10];
};
void rename(struct person *p, const char *new_name) {
strcpy(p->name, new_name);
}
int main(void) {
struct person row[2] = {{42345678, "Pepito"}, {12343278, "Juan"}};
struct person *p = &row[1];
p->id = 11111111;
rename(p, "Ana");
printf("%d %s | %d %s\n", row[0].id, row[0].name, row[1].id, row[1].name);
return 0;
}
Its output:
42345678 Pepito | 11111111 Ana
What to watch step by step:
- When
rowis created, notice that two boxes side by side appear, each withidandname. Thenamearray is shown slot by slot, with the spare\0s. - At
struct person *p = &row[1];,pisn’t a box with fields: it’s an arrow that leavespand points to the second person. - At
p->id = 11111111;the ID ofrow[1]changes, even thoughrowdoesn’t appear on that line. The arrow is the path. - When you enter
rename, the function’s parameterpis another arrow to the same struct (not a copy of the person). Watch how the letters ofrow[1].namechange withstrcpy: “Ana” and its\0, with leftovers of “Juan” behind. - Try changing
renameso it receivesstruct person p(no asterisk) andp.name: you’ll see a new box with the copy appear, androw[1]doesn’t change.
Summary
| Concept | The essentials |
|---|---|
struct | Groups fields of different types; the type is struct name (or an alias with typedef) |
| Assigning | A struct is copied whole with =; an array isn’t, it’s copied with strcpy |
| Memory | Fields in order, with padding for alignment; size always with sizeof |
| Dot and arrow | s.field with the struct; p->field (= (*p).field) with its address |
| String | char array ending in '\0'; char x[N] holds N − 1 letters |
sizeof and strlen | Bytes reserved versus characters up to the null |
string.h | strcpy, strcat, strcmp, strchr… none of them checks whether it fits; snprintf does |
Three ideas to take away:
- A struct is a block of bytes with its fields in a row: if you go past a field, you overwrite the next one, or the struct next door.
- Dot if you have the struct, arrow if you have its address. To modify a struct inside a function, pass it the address.
- **Every string needs room for its
'\0'**, and in C it’s copied withstrcpy/snprintfand compared withstrcmp, never with=or==.
In the structs and strings in C practice we build the whole cinema: bookings that arrive with errors, a viewer finder for badly typed names and a box office that keeps the titles on the heap. See you there!
Next step: practiceStructs and strings in C — practice with a cinema hall, a viewer finder and a box office on the heap (and the Lucía the program swore didn’t exist)Did this article help you? Here is how you can support SergioLearns →

3 Comments