Structs, Pointers, and the Heap
Pass structs to functions by pointer with the arrow operator, build arrays of structs, and allocate structs with malloc: everything from chapters 3 and 4 applied to aggregate data.
The Pointer That Does Not Copy
Lesson 1 stopped one line short of a working halve_price. It received a whole struct Book, halved the price in its own copy, and left the caller's book at 19.99, which is chapter 3's rule that a function gets a copy of its argument, with a 48 byte object standing in the place an int used to hold. Chapter 3 supplied the fix too, and it is the same fix as ever: pass the address. struct Book *p = &pick; declares a pointer to a struct exactly as int *q = &n; declared a pointer to an int, and & and * mean what they have always meant. What is new is how you reach a member through one. (*p).pages is the longhand, dereference first and then select, and those parentheses are not optional, because the dot binds tighter than the *. Without them, *p.pages reads as "select the member pages of p, and then dereference the result", which asks a pointer for a member that no pointer has, and gcc answers by naming the operator you actually wanted:
error: 'p' is a pointer; did you mean to use '->'?
15 | printf("%d\n", *p.pages);
| ^
| ->
#include <stdio.h>
struct Book
{
char title[32];
int pages;
double price;
};
void halve_price(struct Book *book)
{
book->price = book->price / 2.0;
}
void print_shelf(const struct Book *books, int count)
{
for (int i = 0; i < count; ++i)
{
printf("%s, %d pages, $%.2f, at %p\n", books[i].title, books[i].pages, books[i].price, (const void *)&books[i]);
}
}
int main(void)
{
struct Book shelf[3] = {
{.title = "Deep C", .pages = 300, .price = 19.99},
{.title = "Pointers", .pages = 250, .price = 24.50},
{.title = "Sanitizers", .pages = 180, .price = 12.00},
};
struct Book *p = &shelf[0];
int count = sizeof(shelf) / sizeof(shelf[0]);
printf("p->pages is %d, (*p).pages is %d, %d books of %zu bytes each\n", p->pages, (*p).pages, count, sizeof(struct Book));
halve_price(p);
print_shelf(shelf, count);
return 0;
}
p->pages is 300, (*p).pages is 300, 3 books of 48 bytes each
Deep C, 300 pages, $9.99, at 0xfbffa76f0030
Pointers, 250 pages, $24.50, at 0xfbffa76f0060
Sanitizers, 180 pages, $12.00, at 0xfbffa76f0090
p->pages means exactly (*p).pages, one token that dereferences and selects in a single step, and the first output line is the two spellings agreeing. The dot has not gone anywhere: it is what you write when you hold the struct, and the arrow is what you write when you hold its address. halve_price(struct Book *book) is then the function lesson 1 could not write. It receives an address rather than 48 bytes, book->price names the member of the caller's object, and the $9.99 on the second output line is that object genuinely changed rather than a copy discarded at the closing brace. This is chapter 3's out parameter with an aggregate in it. print_shelf is the other half of the pattern, and the half you will write far more often: a function that only reads a struct should receive a const struct Book *. Nothing is copied, which is the whole argument for a pointer once a struct grows past a couple of members, and the const is checked rather than trusted, since assigning to book->price through such a parameter is error: assignment of member 'price' in read-only object. Make that pairing your default. Take a pointer to receive a struct, and make it const unless changing the caller's object is the function's job.
Arrays of Structs
struct Book shelf[3] is chapter 4's array with lesson 1's struct as its element type, and neither half bends to accommodate the other. Each element gets its own brace enclosed initializer, designated inside exactly as a lone struct would be, so a member left out is still zeroed. Reaching a member is index and then dot, shelf[1].title, with no new operator involved: the subscript picks the struct out of the array and the dot picks the member out of the struct. sizeof(shelf) / sizeof(shelf[0]) still counts elements, because shelf is a real array in the scope that declared it, and chapter 4's warning about that idiom is equally unchanged: inside print_shelf the parameter is a pointer, sizeof would measure the pointer, and so the count arrives as its own parameter. print_shelf(const struct Book *books, int count) is chapter 4's pointer and count pair with a struct element type and a const in front, which is the shape nearly every function that works on a collection of structs takes. The three addresses in that output then answer a question lesson 1 left open. They end in 0x30, 0x60 and 0x90, which is 48 bytes apart, and 48 is what the same run reports for sizeof(struct Book) while the members of a struct Book add up to 44. The elements of an array of structs sit sizeof(struct Book) apart, padding included. Chapter 4's contiguity is intact, so books[i] is still *(books + i) and pointer arithmetic still steps by whole elements; the padding changes only how large a step is. It has to, because the padding exists to keep each member on an address its type can live at, and elements spaced 44 bytes apart would leave the second book's double misaligned.
Structs on the Heap
A struct on the heap is malloc doing exactly what it has always done, and the request is chapter 4's sizing idiom back in its full form. struct Book *book = malloc(sizeof *book); asks for the size of one of the things book points at, which is one whole struct, padding included, and it stays right if the struct gains a member tomorrow. Chapter 5 collapsed that idiom to malloc(length + 1) because sizeof(char) is defined to be 1 and multiplying by it changes nothing; nothing collapses here, and sizeof *book is the reason you never have to know the number. Everything else is unchanged: check the result against NULL before touching it, reach members with the arrow because what you hold is an address, and free it exactly once. malloc(count * sizeof *shelf) gives you count structs back to back, indexed with the same shelf[i].pages a stack array took. And make_book is chapter 4's ownership convention with a struct in it, returning a pointer to memory it allocated for a caller who must free it, with the make_ prefix and this sentence as the only mechanism C offers for saying so.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Book
{
char *title;
int pages;
};
struct Book *make_book(const char *title, int pages)
{
struct Book *book = malloc(sizeof *book);
if (book == NULL)
{
return NULL;
}
book->title = malloc(strlen(title) + 1);
if (book->title == NULL)
{
free(book);
return NULL;
}
strcpy(book->title, title);
book->pages = pages;
return book;
}
void free_book(struct Book *book)
{
if (book == NULL)
{
return;
}
free(book->title);
free(book);
}
int main(void)
{
struct Book *book = make_book("Deep C", 300);
struct Book *shelf = malloc(2 * sizeof *shelf);
if (book == NULL || shelf == NULL)
{
printf("could not allocate\n");
free(shelf);
free_book(book);
return 0;
}
shelf[0] = *book;
shelf[1] = *book;
printf("titles at %p %p %p\n", (void *)book->title, (void *)shelf[0].title, (void *)shelf[1].title);
shelf[0].title[0] = 'B';
printf("book->title is now %s, %d pages, %zu bytes per struct\n", book->title, book->pages, sizeof *book);
free(shelf);
free_book(book);
return 0;
}
titles at 0xfc1faa9e0030 0xfc1faa9e0030 0xfc1faa9e0030
book->title is now Beep C, 300 pages, 16 bytes per struct
The Shallow Copy
That struct holds a char *title, which is new, and it changes what copying one means. Assignment still copies every member exactly as lesson 1 described, and for a pointer member, copying the member copies the address. A struct holding a pointer is copied shallowly, and both copies then point at the same object. The first output line is that fact with nothing left to argue about: book->title, shelf[0].title and shelf[1].title are one address printed three times, so there is one seven byte string and there are three structs claiming it, which is why storing a B through shelf[0] changes what book prints on the next line. C has no copy constructor and no deep copy to reach for. If you want a second string you call the copying function yourself, and chapter 5's make_copy is that function; make_book is doing exactly that inline, which is what makes the book it returns genuinely its own.
Now count the frees, because that program has exactly one for the string and sits one line away from a disaster. Adding free(shelf[0].title); above free(shelf) is wrong, and it is the most reasonable looking line you could add, tidying up a title the way free_book is about to:
==12==ERROR: AddressSanitizer: attempting double-free on 0xfc1f959e0030 in thread T0:
#1 0x000000400b40 in free_book /tmp/e.c:41
0xfc1f959e0030 is located 0 bytes inside of 7-byte region [0xfc1f959e0030,0xfc1f959e0037)
freed by thread T0 here:
#1 0x000000400b28 in main /tmp/e.c:65
Line 65 freed the string and line 41, inside free_book, freed it again through the only other pointer to it, the seven byte region being strlen("Deep C") + 1 exactly as chapter 5 taught you to ask for it. Chapter 4's rule needs no amendment, it just needs applying to the right thing: one free per allocation, and a shallow copy is not an allocation. The second half of that is the order, which free_book gets right and which is worth saying out loud. Free the members, then the struct. Writing free(book); free(book->title); instead reads the title pointer out of a block you have already released, so it is a heap-use-after-free on the read rather than a clever shortcut, and gcc warns pointer 'book' used after 'free' before you ever run it. One thing to note in passing, for the data structures you will meet elsewhere: a struct may contain a pointer to its own type, which is how linked lists and trees are built, though it may never contain an instance of itself.
Key Takeaways
p->memberis exactly(*p).member. The parentheses in the longhand are required because the dot binds tighter than the*, so*p.memberis an error naming the arrow you should have used. The dot is for a struct you hold, the arrow for an address you hold.- A
struct T *parameter reaches the caller's object, so a function taking one can change it, unlike lesson 1's by-value copy.const struct T *is the default way to receive a struct you only read: no copy of any size, and no way to modify it by accident. - An array of structs is indexed and then dotted,
shelf[i].pages.sizeof(arr) / sizeof(arr[0])counts elements only in the scope that declared the array, so functions still take a pointer and a count, and elements sitsizeof(struct T)apart, padding included. - On the heap,
malloc(sizeof *p)allocates one struct andmalloc(count * sizeof *p)allocates an array of them. Check againstNULL, use the arrow, free once. Amake_function returning a heap struct hands ownership to its caller. - A struct holding a pointer is copied shallowly: both copies point at the same object. Assignment copies the pointer, not the string, so freeing "both" is
attempting double-freeon one block. Deep copying is a function you call, never something assignment does for you. - Free the members, then the struct:
free(book->title); free(book);. The other order reads a member out of a block that no longer exists, which isheap-use-after-free.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Structs, Pointers, and the Heap - Quiz
Test your understanding of the lesson.
Practice Exercises
A Book That Owns Its Title
Read pairs of a title and a page count until the input runs out, print each book, and give every byte back. Everything except two functions is written for you: main reads with scanf("%31s %d", title, &pages) into one reused buffer, keeps up to four books in an array of pointers, prints the totals, and calls free_book on each one at the end, while print_book shows the shape this lesson asks you to prefer, taking a const struct Book * so that nothing is copied and nothing can be modified by accident. What is missing is make_book and free_book, and between them they are the whole lesson. make_book allocates the struct with malloc(sizeof *book), which is chapter 4's sizing idiom in its full form rather than chapter 5's collapsed malloc(length + 1), and checks the result against NULL before touching it. Then it gives the book a title of its own, because the title it was handed points into main's buffer and that buffer is overwritten by the next scanf: allocate strlen(title) + 1 bytes, the characters plus the terminator that nobody counts, and strcpy into them. Leave the + 1 out and the terminator lands one byte past the end, which is a heap-buffer-overflow with a WRITE and the run ends there. Both allocations get checked, and the second check is the interesting one, since returning NULL at that point without freeing the struct you already have leaks it: clean up what you took before you give up. Reach the members with the arrow, book->title and book->pages, because what you hold is an address and not a struct. free_book undoes all of that and its two rules are order and tolerance. Return immediately when book is NULL, so that a failed make_book can be passed straight to it, and then free the member before the struct, free(book->title); before free(book);, because the other order reads the title pointer out of a block that has already been released, which is a heap-use-after-free rather than a shortcut. fail_on_memory_leak is watching both functions: a book whose title is never freed leaks the string even when every line of output is correct. main returns 0 on every path, including the one that reads nothing at all and prints no books, since the checker treats a nonzero exit status as a failure however correct the printed output looks.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!