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->member is exactly (*p).member. The parentheses in the longhand are required because the dot binds tighter than the *, so *p.member is 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 sit sizeof(struct T) apart, padding included.
  • On the heap, malloc(sizeof *p) allocates one struct and malloc(count * sizeof *p) allocates an array of them. Check against NULL, use the arrow, free once. A make_ 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-free on 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 is heap-use-after-free.