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.

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 just seat a; doesn’t compile (further down you’ll see how to shorten it with typedef).
  • 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

WayExampleWhen 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 fieldc.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:

StructFields and positionSumsizeof
unordereda at 0, (3 padding), b at 4, c at 8, (3 padding)1 + 3 + 4 + 1 + 312
orderedb at 0, a at 4, c at 5, (2 padding)4 + 1 + 1 + 28
personid at 0, name at 4, (2 padding)4 + 10 + 216

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:

The hook overflow byte by byteTwo consecutive 16-byte person structs: id (4), name (10) and 2 padding bytes. Copying Maximiliano Ruiz (17 bytes) into seat 0's name fills the padding, puts the word Ruiz into seat 1's id and the null character into the first byte of its name.Before: row[2] in memory (2 × 16 bytes)····Pepito\0\0\0\0······Juan\0\0\0\0\0\0··4234567812343278row[0]row[1]name[10]paddingAfter strcpy(row[0].name, "Maximiliano Ruiz")····Maximiliano Ruiz\0uan\0\0\0\0\0\0··205373166617 bytes“Ruiz” overwrites the id of row[1] → 2053731666'\0' in row[1].name[0] → empty name
The 17 bytes don't fit in the 10 of name: they keep writing into the padding and the seat next door.

“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:

  1. Bytes 4 to 13: “Maximilian” fills the 10 bytes of name.
  2. Bytes 14 and 15: “o” and the space land in the padding. Nobody notices.
  3. Bytes 16 to 19: “Ruiz” lands in the id of row[1]. Those four bytes (R = 82, u = 117, i = 105, z = 122) read as an int give 2053731666.
  4. Byte 20: the '\0' lands on the first character of row[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:

Dot versus arrowThe variable viewer is a struct with id 42345678 and name Pepito. The pointer p stores its address. viewer.id, (*p).id and p->id reach the same field.struct person viewerid42345678name"Pepito"struct person *p&viewerviewer.id(*p).idp->idall three reach the same intI have the struct → dotI have its address → arrow
With the struct itself you use the dot; with its address, the arrow (short for (*p).).
  • viewer.id: you have the struct, you use the dot.
  • (*p).id: you have a pointer; *p is the struct it points to, and you apply the dot to that struct. The parentheses are compulsory: *p.id means *(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 haveExampleTo reach a field
A structstruct person viewerviewer.id
A pointer to a structstruct person *pp->id
A slot in an array of structsseats[i]seats[i].id (it’s a struct)
An address inside the arrayseats + 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:

A string is a char array ending in '\0'Top, a 10-char array with A, n, a and seven null characters: strlen gives 3 and sizeof 10. Bottom, a 4-char array with A, n, a and the null: sizeof gives 4.char gap[10] = "Ana";A0n1a2\03\04\05\06\07\08\09strlen = 3 (stops at the first '\0')the string ends heresizeof = 10 (everything reserved)char name[] = "Ana";Ana\0sizeof = 4 (3 letters + the null)
strlen counts letters up to the first '\0'; sizeof measures the whole array, null and spare bytes included.
  • char name[] = "Ana"; reserves 4 bytes, not 3: the compiler adds the '\0' on its own. That’s why sizeof(name) gives 4 and strlen(name) gives 3.
  • char gap[10] = "Ana"; reserves 10 bytes; the 7 spare slots are filled with '\0'. sizeof still measures the whole array (10) and strlen stops at the first null (3).
  • length("Pepito") counts 6: the loop while (*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.
ExpressionWhat it measures“Ana” in char gap[10]
sizeof gapBytes reserved for the array (the compiler knows it)10
strlen(gap)Characters up to the first '\0' (counted at run time)3
What fitssizeof - 1 characters, because the null takes up space too9

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. Trying film[0] = 'K' compiles (if you remove the const), but the program dies with Segmentation fault. That’s why I added const: 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).

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
FunctionWhat it doesWatch 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 includedDoesn’t check that it fits: that’s what happened in the hook
strcat(dst, src)Appends src to the end of dstdst must have room for both and the null
strcmp(a, b)Compares letter by letter: 0 if equalOnly the sign matters, not the number
strncpy(dst, src, n)Copies at most n bytesIf it doesn’t fit, it **doesn’t add the '\0'**
snprintf(dst, n, fmt, ...)Writes with a format, at most n bytesAlways ends in '\0'; returns what it would have needed
strchr(s, c)Pointer to the first c in s, or NULLYou have to check for NULL before using it

Let’s go through what the output says:

  • strcpy and strcat: greeting started as “Hello, ” and ended up as “Hello, Lucia!”. strcat starts writing exactly where the '\0' of greeting was and adds a new one at the end. It works because greeting has 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_label wouldn’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. And pc + 1 points 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?

SituationWhat to useWhy
Several pieces of data that go together (a seat, a student)structOne name, passed and copied in one go
A function that changes the structstruct x *p and p->fieldBy value you’d change a copy
A function that only reads itconst struct x *pNo copy, and the compiler protects the original
Text with a known maximum sizechar name[N] inside the structSimple; remember N − 1 letters fit
Text whose size varies a lotchar * + malloc(strlen(s) + 1)Each text takes exactly what it needs (you’ll do it in the practice)
Copying text that might not fitsnprintf(dst, sizeof dst, "%s", src)Never goes over and always adds the '\0'
Comparing textstrcmp(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:

  1. When row is created, notice that two boxes side by side appear, each with id and name. The name array is shown slot by slot, with the spare \0s.
  2. At struct person *p = &row[1];, p isn’t a box with fields: it’s an arrow that leaves p and points to the second person.
  3. At p->id = 11111111; the ID of row[1] changes, even though row doesn’t appear on that line. The arrow is the path.
  4. When you enter rename, the function’s parameter p is another arrow to the same struct (not a copy of the person). Watch how the letters of row[1].name change with strcpy: “Ana” and its \0, with leftovers of “Juan” behind.
  5. Try changing rename so it receives struct person p (no asterisk) and p.name: you’ll see a new box with the copy appear, and row[1] doesn’t change.

Summary

ConceptThe essentials
structGroups fields of different types; the type is struct name (or an alias with typedef)
AssigningA struct is copied whole with =; an array isn’t, it’s copied with strcpy
MemoryFields in order, with padding for alignment; size always with sizeof
Dot and arrows.field with the struct; p->field (= (*p).field) with its address
Stringchar array ending in '\0'; char x[N] holds N − 1 letters
sizeof and strlenBytes reserved versus characters up to the null
string.hstrcpy, strcat, strcmp, strchr… none of them checks whether it fits; snprintf does

Three ideas to take away:

  1. 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.
  2. Dot if you have the struct, arrow if you have its address. To modify a struct inside a function, pass it the address.
  3. **Every string needs room for its '\0'**, and in C it’s copied with strcpy/snprintf and compared with strcmp, 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 →

Sigue aprendiendo

3 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *