Structs and strings in C — exercises with the movie listings, your own string.h functions and viewer records that arrive broken (and the VIP ticket that always cost 8 euros)
While preparing for the IC2 midterm I wrote a little box-office program: if the customer asked for a “VIP” ticket, it cost 12 euros; otherwise, 8. I tested it with price("VIP") and it gave 12. Perfect. Then I changed the program to store the customer’s choice in an array and, suddenly, every VIP ticket cost 8. The comparison code was the same. It took me an afternoon to understand that I had never been comparing letters: I was comparing addresses, and the first time I got it right by pure luck. You’ll see it live in fragment 2 of “What’s wrong here?”.
These exercises are the third part of the topic, after the theory and the structs and strings in C practice. Here I no longer give you the code first: you try it yourself first.
Table of Contents
How to use these exercises
- Read the statement and the expected output. Your program has to give exactly that output with that data.
- Try to solve it without looking at the solution. If you’re stuck for more than 10 minutes, open the hint.
- Compile with
gcc -Wall -std=c11and don’t consider the exercise done if there are warnings. - Then do “What’s wrong here?”: try to predict the output before reading the explanation.
- Compare with the solutions: there isn’t just one good way, but pay attention to the decisions (why
>=and not>, why a pointer and not a copy…).
At the end you have the cheat sheet with everything from the topic on one page.
Level 1: the movie listings
Declare a struct movie with a title of up to 29 characters, the length in minutes and the room (a letter). Create an array with these listings: Coco (105 min, room A), Interstellar (169 min, room B), Up (96 min, room A) and La sociedad de la nieve (144 min, room C). The program has to:
- Print each movie with its room and its length in hours and minutes (minutes always with two digits).
- Say which movie is the longest.
- Say which one has the longest title and how many characters it has.
Expected output:
Room A Coco 1 h 45 min Room B Interstellar 2 h 49 min Room A Up 1 h 36 min Room C La sociedad de la nieve 2 h 24 min The longest: Interstellar (169 min) Longest title: La sociedad de la nieve (23 characters)
The hours are minutes / 60 (integer division) and the leftover minutes, minutes % 60. For the longest title don’t use sizeof(title): it would give 30 for all of them.
Level 2: your own string.h functions
Write these three functions without using string.h (except to compare results at the end), walking the strings with pointers:
size_t my_strlen(const char *s): how many characters there are up to the'\0'.char *my_strcpy(char *dest, const char *src): copiessrcintodest, null included, and returnsdest.int my_strcmp(const char *a, const char *b): 0 if they’re equal, negative ifagoes first and positive if it goes after. It returns the difference of the codes of the first different character.
Test them with the pairs (“Ana”, “Ana”), (“Ana”, “Anabel”), (“Bruno”, “Ana”) and (“ana”, “Ana”), comparing the sign of your my_strcmp with that of strcmp, and copy “Pepito” into a 10-char array with your my_strcpy.
Expected output:
Ana Ana my_strcmp = 0 (sign 0) | strcmp sign 0 Ana Anabel my_strcmp = -98 (sign -1) | strcmp sign -1 Bruno Ana my_strcmp = 1 (sign 1) | strcmp sign 1 ana Ana my_strcmp = 32 (sign 1) | strcmp sign 1 my_strcpy -> Pepito, my_strlen = 6, strlen = 6
In “Ana” against “Anabel” there’s no different letter in the first 3 positions. The difference is in the fourth: the '\0' of “Ana” against the ‘b’ of “Anabel”. That’s why the loop has to stop when the first string ends, not only when the letters differ.
Level 3: viewer records that arrive broken
The cinema system exports each viewer as a line of text with the format ID;Name;Seat, for example 42345678;Pepito;A3. Write int read_record(const char *line, struct record *f) that splits the line and fills in a struct record (ID number as an int, name of up to 11 characters and seat of up to 3), returning 0 if everything is fine or an error code:
| Code | Error |
|---|---|
| 1 | The ; are missing |
| 2 | The ID number doesn’t have exactly 8 digits |
| 3 | The name is empty or doesn’t fit |
| 4 | The seat has fewer than 2 characters or doesn’t fit |
Process these six lines, keep only the valid ones, report each error and find who’s in C12:
42345678;Pepito;A3 12343278;Juan;C12 1234567;Ana;B1 33333333;Maximiliano Ruiz;C6 44444444 Lucia A4 55555555;Ivan;
Expected output:
Line 3 (1234567;Ana;B1): invalid ID number Line 4 (33333333;Maximiliano Ruiz;C6): name empty or too long Line 5 (44444444 Lucia A4): missing the ';' Line 6 (55555555;Ivan;): invalid seat Valid records: 2 of 6 A3 42345678 Pepito C12 12343278 Juan In C12 sits Juan
strchr(line, ';') gives you a pointer to the first ; and strchr(p1 + 1, ';') to the second. By subtracting pointers you know how long each field is before copying anything. The name doesn’t end in '\0' inside the line: copy it with memcpy and add the null yourself.
What’s wrong here?
Three fragments with a typical mistake from the topic. Read each one, predict what it prints and then look at the real output and the explanation.
Fragment 1: the label that was “plenty”
#include <stdio.h>
#include <string.h>
// Prints the label that is stuck on the seat
void label(const char *name) {
// "Plenty for short names"
char short_label[6];
strcpy(short_label, name);
printf("Label: %s\n", short_label);
}
int main(void) {
label("Ana");
label("Pepito");
return 0;
}
What does it print? Think about it before reading on. I compiled it with AddressSanitizer (-g -fsanitize=address) and this is what came out:
Label: Ana
==386==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7f258f300066
WRITE of size 7 at 0x7f258f300066 thread T0
#1 in label fragment.c:8
#2 in main fragment.c:14
Address 0x7f258f300066 is located in stack of thread T0 at offset 38 in frame
#0 in label fragment.c:5
This frame has 1 object(s):
[32, 38) 'short_label' (line 7) <== Memory access at offset 38 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow ../../../../src/libsanitizer/asan/asan_interceptors.cpp:563 in strcpy
The first label comes out fine: “Ana” is 3 letters + '\0' = 4 bytes, and they fit in 6. The second one blows up: “Pepito” is 6 letters, exactly the size of short_label… but strcpy copies the null too, 7 bytes. AddressSanitizer spells it out: WRITE of size 7, on line 8 (the strcpy), and the access lands at offset 38, right after the 6 bytes of short_label (which go from 32 to 37).
Without AddressSanitizer, this program could print “Label: Pepito” and look correct: the extra byte lands in stack memory nobody is using at that moment. That’s why these bugs are so treacherous. The fix:
#include <stdio.h>
void label(const char *name) {
char short_label[12];
// snprintf never writes more than sizeof short_label bytes, '\0' included
snprintf(short_label, sizeof short_label, "%s", name);
printf("Label: %s\n", short_label);
}
int main(void) {
label("Ana");
label("Pepito");
return 0;
}
With snprintf(short_label, sizeof short_label, "%s", name) it’s impossible to go over: if the name didn’t fit, it would be cut. And the array now has 12 bytes for normal names. Fixed output:
Label: Ana Label: Pepito
Fragment 2: the VIP ticket
#include <stdio.h>
double price(const char *type) {
if (type == "VIP")
return 12.0;
return 8.0;
}
int main(void) {
// What the customer chose, stored in an array of its own
char chosen[] = "VIP";
printf("Ticket %s: %.2f euros\n", chosen, price(chosen));
return 0;
}
How much does the ticket cost? When compiling, gcc already warns:
fragment.c: In function 'price': fragment.c:4:14: warning: comparison with string literal results in unspecified behavior [-Waddress]
And the output:
Ticket VIP: 8.00 euros
The customer chose “VIP” and pays 8 euros. type == "VIP" compares two addresses: that of the array chosen (on main‘s stack) and that of the literal “VIP” (in read-only memory). They’re never the same, so the condition is always false.
And why did it work at first in my story? Because I was calling price("VIP") with a literal, and gcc usually stores identical literals only once: both “VIP”s were the same pointer. It worked by chance, which is what the warning calls unspecified behavior. The fix is to compare letters:
#include <stdio.h>
#include <string.h>
double price(const char *type) {
// strcmp compares letter by letter; == would only compare addresses
if (strcmp(type, "VIP") == 0)
return 12.0;
return 8.0;
}
int main(void) {
char chosen[] = "VIP";
printf("Ticket %s: %.2f euros\n", chosen, price(chosen));
return 0;
}
Now the warning disappears and the VIP customer pays the right price:
Ticket VIP: 12.00 euros
Fragment 3: the seat that wouldn’t get taken
#include <stdio.h>
struct seat {
char row;
int number;
int taken;
};
void occupy(struct seat b) {
b.taken = 1;
printf("Inside occupy: %c%d taken=%d\n", b.row, b.number, b.taken);
}
int main(void) {
struct seat b = {'B', 4, 0};
occupy(b);
printf("In main: %c%d taken=%d\n", b.row, b.number, b.taken);
return 0;
}
What value does taken have in main? The output:
Inside occupy: B4 taken=1 In main: B4 taken=0
Inside occupy, the seat is taken; in main, it isn’t. occupy receives the struct by value: C copies the 12 bytes of b into the parameter, the function changes the copy and the copy disappears on return. The parameter having the same name as the variable in main changes nothing: they’re two different seats. The fix is to pass the address and use the arrow:
#include <stdio.h>
struct seat {
char row;
int number;
int taken;
};
// Receives the seat's ADDRESS: the changes reach the one in main
void occupy(struct seat *b) {
b->taken = 1;
printf("Inside occupy: %c%d taken=%d\n", b->row, b->number, b->taken);
}
int main(void) {
struct seat b = {'B', 4, 0};
occupy(&b);
printf("In main: %c%d taken=%d\n", b.row, b.number, b.taken);
return 0;
}
With the address, the change reaches main:
Inside occupy: B4 taken=1 In main: B4 taken=1
“fixing” fragment 3 by making occupy return the seat (b = occupy(b);). It works, but it copies the struct twice on every call. With a pointer nothing is copied.
Solutions
Level 1 solution
#include <stdio.h>
#include <string.h>
struct movie {
char title[30]; // 29 characters at most + '\0'
int minutes;
char room;
};
int main(void) {
struct movie listings[] = {
{"Coco", 105, 'A'},
{"Interstellar", 169, 'B'},
{"Up", 96, 'A'},
{"La sociedad de la nieve", 144, 'C'},
};
int n = sizeof listings / sizeof listings[0];
// We store POSITIONS, not copies: that way we reach the original struct later
int longest = 0, longest_title = 0;
for (int i = 0; i < n; i++) {
// Pointer so we can write p->field instead of listings[i].field
struct movie *p = &listings[i];
// Integer division: 169 / 60 = 2 hours; the remainder, 169 % 60 = 49 minutes
printf("Room %c %-24s %d h %02d min\n", p->room, p->title, p->minutes / 60, p->minutes % 60);
if (p->minutes > listings[longest].minutes)
longest = i;
// strlen counts characters up to the '\0'; sizeof would always give 30
if (strlen(p->title) > strlen(listings[longest_title].title))
longest_title = i;
}
printf("The longest: %s (%d min)\n", listings[longest].title, listings[longest].minutes);
printf("Longest title: %s (%zu characters)\n", listings[longest_title].title, strlen(listings[longest_title].title));
return 0;
}
The decisions:
- **I store positions (
longest,longest_title), not copies.** They start at 0 (the first movie) and change when a better one appears. At the end,listings[longest]is the original struct. Copying the whole struct each time would also work, but that’s 40 bytes (30 for the title + 4 for theint+ 1 for the room + 3 of padding) on every change, for nothing. - **
struct movie *p = &listings[i];** is just for comfort:p->minutesreads better thanlistings[i].minutesrepeated four times. - **
>and not>=.** With>=, if two movies were equally long, the last one would win; with>, the first. There’s no tie here, but it’s worth deciding on purpose. - **
%02d** pads with a zero on the left: 1 h 05 min instead of 1 h 5 min.
The sums in the output: 105 / 60 = 1 and 105 % 60 = 45 → 1 h 45 min; 169 / 60 = 2 and 169 − 120 = 49 → 2 h 49 min; 96 → 1 h 36 min; 144 → 2 h 24 min. The longest is Interstellar (169 > 144 > 105 > 96). In titles: Coco 4, Interstellar 12, Up 2 and “La sociedad de la nieve” (the Spanish film Society of the Snow, under its original title) 23, with its 4 spaces, which are characters too.
Level 2 solution
#include <stdio.h>
#include <string.h>
// Counts up to the '\0'
size_t my_strlen(const char *s) {
const char *p = s;
while (*p != '\0')
p++;
// The distance between two pointers is the number of characters walked
return p - s;
}
// Copies the '\0' too: that's why the loop checks the character AFTER copying it
char *my_strcpy(char *dest, const char *src) {
char *d = dest;
while ((*d++ = *src++) != '\0')
;
// Like the real one, returns dest
return dest;
}
// Moves on while they are equal and the first one has not ended; returns the difference of the first different character
int my_strcmp(const char *a, const char *b) {
while (*a != '\0' && *a == *b) {
a++;
b++;
}
// unsigned char: so 'ñ' or other high bytes do not come out negative
return (unsigned char)*a - (unsigned char)*b;
}
// Only the sign matters: the string.h one may return another number with the same sign
int sign(int x) {
return (x > 0) - (x < 0);
}
int main(void) {
const char *tests[][2] = {
{"Ana", "Ana"}, {"Ana", "Anabel"}, {"Bruno", "Ana"}, {"ana", "Ana"},
};
for (int i = 0; i < 4; i++) {
const char *a = tests[i][0], *b = tests[i][1];
printf("%-6s %-7s my_strcmp = %4d (sign %2d) | strcmp sign %2d\n",
a, b, my_strcmp(a, b), sign(my_strcmp(a, b)), sign(strcmp(a, b)));
}
char copy[10];
my_strcpy(copy, "Pepito");
printf("my_strcpy -> %s, my_strlen = %zu, strlen = %zu\n", copy, my_strlen(copy), strlen(copy));
return 0;
}
Let’s go function by function:
// Counts up to the '\0'
size_t my_strlen(const char *s) {
const char *p = s;
while (*p != '\0')
p++;
// The distance between two pointers is the number of characters walked
return p - s;
}
my_strlen moves a second pointer p up to the null and returns p - s: subtracting two char pointers gives how many characters lie between them. For “Pepito”, p ends at position 6 and 6 − 0 = 6.
// Copies the '\0' too: that's why the loop checks the character AFTER copying it
char *my_strcpy(char *dest, const char *src) {
char *d = dest;
while ((*d++ = *src++) != '\0')
;
// Like the real one, returns dest
return dest;
}
my_strcpy is the most compact line in the article: *d++ = *src++ copies one character and moves both pointers forward, and the comparison with '\0' happens after copying. So when the null arrives, it’s copied first and then the loop stops: the destination is properly terminated. The body of the while is empty (the ; alone on its line) because all the work is in the condition. For “Pepito”:
| Round | Character copied | Was it ‘\0’? | dest so far |
|---|---|---|---|
| 1 | P | no | P |
| 2 | e | no | Pe |
| 3-6 | p, i, t, o | no | Pepito |
| 7 | ‘\0’ | yes → stops | Pepito (terminated) |
// Moves on while they are equal and the first one has not ended; returns the difference of the first different character
int my_strcmp(const char *a, const char *b) {
while (*a != '\0' && *a == *b) {
a++;
b++;
}
// unsigned char: so 'ñ' or other high bytes do not come out negative
return (unsigned char)*a - (unsigned char)*b;
}
my_strcmp moves forward while the characters are equal and the first string hasn’t ended. If a ends, *a is '\0'; if b ends first, *a == *b fails (a letter against 0). Either way the loop stops and the subtraction gives the result. The sums in the output:
| a | b | First different character | Subtraction | Sign |
|---|---|---|---|---|
| Ana | Ana | none (both '\0') | 0 − 0 = 0 | 0 |
| Ana | Anabel | position 3: '\0' and ‘b’ | 0 − 98 = −98 | −1 |
| Bruno | Ana | position 0: ‘B’ and ‘A’ | 66 − 65 = 1 | 1 |
| ana | Ana | position 0: ‘a’ and ‘A’ | 97 − 65 = 32 | 1 |
The signs match those of strcmp in all four pairs. The numbers don’t have to match (the standard only promises the sign); that’s why main compares signs with the sign function, which uses a trick: (x > 0) - (x < 0) is 1, 0 or −1 because each comparison is 1 or 0.
The cast to unsigned char in the subtraction isn’t decoration. Characters above 127 (like the bytes of an ‘ñ’ in UTF-8) are negative in a signed char, and the comparison would come out the wrong way round. strcmp always compares as unsigned char.
Level 3 solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
struct record {
int id;
char name[12]; // 11 characters + '\0'
char seat[4]; // 3 characters + '\0'
};
// Reads "ID;Name;Seat". Returns 0 if all is well or the error number
int read_record(const char *line, struct record *f) {
// strchr returns a pointer to the ';' (or NULL): that way we know where each field ends
const char *p1 = strchr(line, ';');
if (p1 == NULL)
return 1;
const char *p2 = strchr(p1 + 1, ';');
if (p2 == NULL)
return 1;
// Subtracting pointers gives how many characters lie between them
size_t id_len = p1 - line;
if (id_len != 8)
return 2;
for (const char *c = line; c < p1; c++)
if (!isdigit((unsigned char)*c))
return 2;
size_t name_len = p2 - (p1 + 1);
// >= and not >: there must be room for the '\0'
if (name_len == 0 || name_len >= sizeof f->name)
return 3;
size_t seat_len = strlen(p2 + 1);
if (seat_len < 2 || seat_len >= sizeof f->seat)
return 4;
// We only write into f once everything is checked
// atoi reads digits and stops by itself at the ';'
f->id = atoi(line);
// The name does not end in '\0' inside the line: we copy name_len bytes and add it ourselves
memcpy(f->name, p1 + 1, name_len);
f->name[name_len] = '\0';
// The seat is the end of the line: it already has its '\0'
strcpy(f->seat, p2 + 1);
return 0;
}
int main(void) {
const char *lines[] = {
"42345678;Pepito;A3",
"12343278;Juan;C12",
"1234567;Ana;B1",
"33333333;Maximiliano Ruiz;C6",
"44444444 Lucia A4",
"55555555;Ivan;",
};
const char *errors[] = {"", "missing the ';'", "invalid ID number", "name empty or too long", "invalid seat"};
int n = sizeof lines / sizeof lines[0];
struct record records[6];
int valid = 0;
for (int i = 0; i < n; i++) {
// &records[valid]: the next free slot; if the line fails, it gets reused
int e = read_record(lines[i], &records[valid]);
if (e == 0)
valid++;
else
printf("Line %d (%s): %s\n", i + 1, lines[i], errors[e]);
}
printf("Valid records: %d of %d\n", valid, n);
for (int i = 0; i < valid; i++)
printf(" %-3s %08d %s\n", records[i].seat, records[i].id, records[i].name);
// Who is in C12?
for (int i = 0; i < valid; i++)
if (strcmp(records[i].seat, "C12") == 0)
printf("In C12 sits %s\n", records[i].name);
return 0;
}
The key function, zoomed in:
// Reads "ID;Name;Seat". Returns 0 if all is well or the error number
int read_record(const char *line, struct record *f) {
// strchr returns a pointer to the ';' (or NULL): that way we know where each field ends
const char *p1 = strchr(line, ';');
if (p1 == NULL)
return 1;
const char *p2 = strchr(p1 + 1, ';');
if (p2 == NULL)
return 1;
// Subtracting pointers gives how many characters lie between them
size_t id_len = p1 - line;
if (id_len != 8)
return 2;
for (const char *c = line; c < p1; c++)
if (!isdigit((unsigned char)*c))
return 2;
size_t name_len = p2 - (p1 + 1);
// >= and not >: there must be room for the '\0'
if (name_len == 0 || name_len >= sizeof f->name)
return 3;
size_t seat_len = strlen(p2 + 1);
if (seat_len < 2 || seat_len >= sizeof f->seat)
return 4;
// We only write into f once everything is checked
// atoi reads digits and stops by itself at the ';'
f->id = atoi(line);
// The name does not end in '\0' inside the line: we copy name_len bytes and add it ourselves
memcpy(f->name, p1 + 1, name_len);
f->name[name_len] = '\0';
// The seat is the end of the line: it already has its '\0'
strcpy(f->seat, p2 + 1);
return 0;
}
The idea is to measure before copying. First the two ; are located with strchr and the lengths are worked out by subtracting pointers; only if everything is valid does it write into f. That way, if a line fails, the struct is left untouched and the next line can reuse the same slot (that’s why main passes &records[valid]).
The decisions:
- **
>=in the lengths.**namehas 12 bytes: 11 letters and the null fit. A 12-letter name no longer fits, which is why it’sname_len >= sizeof f->nameand not>. The same with the seat: “C12” (3) fits in 4; “C123” doesn’t. - **
memcpy+ null for the name.** Inside the line, the name is followed by;, not by'\0', sostrcpywould copy up to the end of the line.memcpycopies exactlyname_lenbytes and then we add the null ourselves. - **
strcpyfor the seat.** The seat is the last thing on the line and already ends in'\0', and its length has been checked: herestrcpyis safe. - **
atoi(line)** converts the digits at the start and stops by itself at the first character that isn’t a digit, the;. We’ve already checked there are 8 digits, so the number is correct. - **
isdigit((unsigned char)*c)**, with the same cast astolowerin the practice.
Line by line:
| Line | p1 at | p2 at | Check that fails | Result |
|---|---|---|---|---|
| 42345678;Pepito;A3 | 8 | 15 | none (8 digits, 6 letters, 2 for the seat) | valid |
| 12343278;Juan;C12 | 8 | 13 | none (4 letters, 3 for the seat < 4) | valid |
| 1234567;Ana;B1 | 7 | 11 | id_len = 7 ≠ 8 | error 2 |
| 33333333;Maximiliano Ruiz;C6 | 8 | 25 | name_len = 25 − 9 = 16 ≥ 12 | error 3 |
| 44444444 Lucia A4 | NULL | — | there’s no ; | error 1 |
| 55555555;Ivan; | 8 | 13 | strlen(“”) = 0 < 2 | error 4 |
2 valid out of 6, as the output says. The %08d prints the ID number with 8 digits, padding with zeros (not needed here, but an ID like 01234567 would lose its zero without it). The final search compares the seat with strcmp(..., "C12") == 0 and finds Juan.
Using strtok to split the line. It works, but it modifies the string (it turns each ; into '\0'), so it doesn’t work with a const line or with literals, and it skips empty fields: with “55555555;Ivan;” you wouldn’t notice the missing seat.
Watch it step by step in Python Tutor
In Python Tutor choose C and paste this program, which brings together fragment 3 and its fix:
#include <stdio.h>
struct seat {
char row;
int number;
int taken;
};
void occupy_copy(struct seat b) {
b.taken = 1;
}
void occupy(struct seat *b) {
b->taken = 1;
}
int main(void) {
struct seat b = {'B', 4, 0};
occupy_copy(b);
printf("After occupy_copy: %d\n", b.taken);
occupy(&b);
printf("After occupy: %d\n", b.taken);
return 0;
}
Its output:
After occupy_copy: 0 After occupy: 1
What to watch step by step:
- In
main, the seatbappears as a box withrow,numberandtakenat 0. - When you enter
occupy_copy, a second frame appears with its own boxb, with the same values: it’s the copy. - When
b.taken = 1;runs, the box inoccupy_copy‘s frame changes; the one inmainstays at 0. On return, that frame disappears and the change with it. - When you enter
occupy, the parameterbis no longer a box: it’s an arrow pointing tomain‘s seat. b->taken = 1;changesmain‘s box directly. That’s why the second line of the output says 1.
Cheat sheet: structs and strings in C
The essentials in 30 seconds
- A
structgroups fields of different types; the type isstruct name. - Dot with the struct (
s.field), arrow with its address (p->field). - To modify a struct in a function, pass it its address.
- A string is a
chararray ending in'\0';char x[N]holds N − 1 letters. - Text: copied with
strcpy/snprintfand compared withstrcmp, never with=or==.
Structs
| I want to… | Code | Result |
|---|---|---|
| Declare the type | struct seat { char row; int number; }; | Template (takes no memory) |
| Create and initialise | struct seat b = {'C', 7}; | b.row = ‘C’, b.number = 7 |
| Initialise by name | struct seat b = {.number = 7}; | row = 0, number = 7 |
| Everything at zero | struct seat room[3][6] = {0}; | Free and empty names |
| Copy it whole | d = b; | Copies every field, arrays included |
| Alias for the type | typedef struct seat Seat; | Seat b; |
| Real size | sizeof(struct seat) | Padding included |
| Pointer and field | struct seat *p = &b; p->number = 8; | Changes b |
| On the heap | malloc(n * sizeof *v) | n structs in a row |
Strings and string.h
| I want to… | Code | Result |
|---|---|---|
| Length | strlen("Ana") | 3 |
| Bytes reserved | sizeof(char[10]) | 10 |
| Copy | strcpy(dst, "Ana") | dst = “Ana” (size not checked) |
| Copy without overflowing | snprintf(dst, sizeof dst, "%s", src) | Cuts if it doesn’t fit and adds '\0' |
| Append to the end | strcat(greeting, "Lucia") | “Hello, Lucia” |
| Compare | strcmp(a, b) == 0 | Equal |
| Does it start with? | strncmp(s, "ana", 3) == 0 | Compares 3 characters |
| Find a character | strchr("42345678;Pepito", ';') | Pointer to position 8 |
| Copy n bytes | memcpy(dst, src, n); dst[n] = '\0'; | A piece of a string |
| Duplicate on the heap | malloc(strlen(s) + 1) + strcpy | Your own copy of the text |
Templates from the exercises
| Exercise | Pattern | Result with the article’s data |
|---|---|---|
| Level 1 | Walk an array of structs keeping the index of the best one | Interstellar (169 min); title: La sociedad de la nieve (23) |
| Level 2 | while (*p) p++; and while ((*d++ = *s++) != '\0'); | my_strlen(“Pepito”) = 6; my_strcmp(“ana”, “Ana”) = 32 |
| Level 3 | strchr for the ; + subtract pointers + memcpy + '\0' | 2 valid records out of 6; Juan is in C12 |
Symptom and likely cause
| Symptom | Likely cause |
|---|---|
assignment to expression with array type | You did s.name = "...": use strcpy or snprintf |
'p' is a pointer; did you mean to use '->'? | Dot with a pointer: use p->field |
invalid type argument of '->' | Arrow with a struct: use s.field |
comparison with string literal results in unspecified behavior | You’re comparing strings with ==: use strcmp |
| A piece of data in another field or another seat changes by itself | An array overflow (no room left for the '\0') |
| Garbage at the end when printing a string | The '\0' is missing (for example, after strncpy) |
The function changes the struct, but main doesn’t see it | You passed the struct by value: pass its address |
stack-buffer-overflow or heap-buffer-overflow (ASan) | You’re writing more bytes than you reserved; check the + 1 for the null |
Before handing it in, ask yourself
- Does every
chararray have room for the'\0'? - Do the functions that modify a struct receive its address?
- Do I compare strings with
strcmpand copy withstrcpy/snprintfknowing they fit? - Does it compile without warnings with
-Walland run clean with-fsanitize=address?
That wraps up structs and strings in C. The next IC2 topic is the preprocessor: #define, macros and conditional compilation. In the meantime, if you want to review where all this comes from, go back to pointers in C and dynamic memory in C.
Did this article help you? Here is how you can support SergioLearns →
