<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pablo GS</title>
    <description>The latest articles on DEV Community by Pablo GS (@pablogs).</description>
    <link>https://dev.to/pablogs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3957976%2Fa148d74f-0c1f-43fb-bf77-6e380bb8c6c0.png</url>
      <title>DEV Community: Pablo GS</title>
      <link>https://dev.to/pablogs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pablogs"/>
    <language>en</language>
    <item>
      <title>Growing Pains: realloc and Automatic Capacity Management</title>
      <dc:creator>Pablo GS</dc:creator>
      <pubDate>Mon, 01 Jun 2026 10:06:10 +0000</pubDate>
      <link>https://dev.to/pablogs/growing-pains-realloc-and-automatic-capacity-management-31hg</link>
      <guid>https://dev.to/pablogs/growing-pains-realloc-and-automatic-capacity-management-31hg</guid>
      <description>&lt;p&gt;&lt;em&gt;Post 2 of the Dynamic Arrays in C series · &lt;a href="https://github.com/ansuzgs/dynamic-arrays-c/blob/main/src/post_02.c" rel="noopener noreferrer"&gt;Full source code&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where We Left Off
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://pablogs.dev/posts/post-01-hello-array/" rel="noopener noreferrer"&gt;Post 1&lt;/a&gt; we built an array that does three things: allocate a fixed buffer, push integers into it, and free everything when we're done. It works, until it doesn't. The moment the user pushes one element more than the initial capacity allows, &lt;code&gt;array_push&lt;/code&gt; returns -1 and refuses to cooperate. The array is full and there's nothing we can do about it.&lt;/p&gt;

&lt;p&gt;That's not a dynamic array. It's a fixed-size buffer with a nice API around it. A real dynamic array solves the fundamental problem: &lt;em&gt;the user doesn't know how many elements they'll need.&lt;/em&gt; Maybe it's 5, maybe 5 million. The array should handle either case without the caller worrying about capacity.&lt;/p&gt;

&lt;p&gt;The mechanism that makes this possible is &lt;code&gt;realloc&lt;/code&gt;. It's one function call, one line of code, and the single most misunderstood function in the C standard library. Most C programmers know what it does in the abstract, "it resizes an allocation." Fewer understand the two distinct things it can do under the hood, why that distinction matters for correctness, and why writing &lt;code&gt;arr-&amp;gt;data = realloc(arr-&amp;gt;data, new_size)&lt;/code&gt; is a bug waiting to happen.&lt;/p&gt;

&lt;p&gt;This post replaces &lt;a href="https://pablogs.dev/posts/post-01-hello-array/" rel="noopener noreferrer"&gt;Post 1&lt;/a&gt;'s static &lt;code&gt;array_push&lt;/code&gt; with one that grows automatically. When size hits capacity, we double the buffer, copy the data, and keep going. The user never has to think about capacity again, they just push.&lt;/p&gt;

&lt;p&gt;But automatic growth has consequences. The most important one is &lt;em&gt;pointer invalidation&lt;/em&gt;: any pointer you held into the old buffer becomes a dangling pointer after realloc. This isn't a theoretical concern, it's one of the most common sources of use-after-free bugs in C codebases. We'll see it happen, understand why, and learn the pattern that prevents it.&lt;/p&gt;

&lt;p&gt;We'll also see the "temporary pointer" pattern, the correct way to call realloc so that a failed allocation doesn't corrupt your array. It's three lines of code, and it's the difference between an array that degrades gracefully on out-of-memory and one that leaks your data and crashes.&lt;/p&gt;

&lt;p&gt;By the end of this post you'll have an array that grows on demand, and you'll understand the two things about realloc that most tutorials get wrong: that it might move your data, and that you must never assign its result directly to the pointer you're reallocating.&lt;/p&gt;

&lt;h2&gt;
  
  
  What realloc Actually Does
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;realloc&lt;/code&gt; function has a deceptively simple signature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;realloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;new_size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You pass it a pointer to an existing allocation (from &lt;code&gt;malloc&lt;/code&gt; or a previous &lt;code&gt;realloc&lt;/code&gt;) and a new size. It returns a pointer to a block of at least &lt;code&gt;new_size&lt;/code&gt; bytes, with the old data preserved. But behind that simple interface, two fundamentally different things can happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 1: extend in-place.&lt;/strong&gt; If there's enough free space right after your current allocation in the heap, the allocator just expands the block. The pointer doesn't change. This is fast, no copying, no new allocation. It's also the case you can never count on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 2: allocate, copy, free.&lt;/strong&gt; If there isn't room to grow in-place (because another allocation sits right after yours), the allocator mallocs a new block of the requested size, copies your old data into it with the equivalent of &lt;code&gt;memcpy&lt;/code&gt;, and frees the old block. The returned pointer is different from the one you passed in. The old pointer is now invalid, the memory it pointed to has been returned to the allocator.&lt;/p&gt;

&lt;p&gt;You cannot predict which case will happen. It depends on the heap's internal state, which allocator your system uses (glibc, jemalloc, musl), how fragmented memory is, and the phase of the moon. Your code must handle both cases correctly. This means one thing: &lt;em&gt;never assume the pointer stays the same after realloc.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There's a third case too: failure. If the system can't satisfy the request, realloc returns &lt;code&gt;NULL&lt;/code&gt;. And here's the critical detail: &lt;strong&gt;on failure, the original block is not freed.&lt;/strong&gt; Your old pointer is still valid, and your old data is still there. This is actually good, it means you can recover gracefully. But only if you don't overwrite your pointer before checking for NULL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;The full file compiles with zero warnings under &lt;code&gt;gcc -Wall -Wextra -Wpedantic -std=c11&lt;/code&gt;, produces ASCII visualization to stdout, and writes a Graphviz DOT file for diagram generation. Here are the essential changes from &lt;a href="https://pablogs.dev/posts/post-01-hello-array/" rel="noopener noreferrer"&gt;Post 1&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The complete source, including the &lt;code&gt;main()&lt;/code&gt; with growth demonstrations, the pointer invalidation demo, and the DOT generator, is available &lt;a href="https://github.com/ansuzgs/dynamic-arrays-c/blob/main/src/post_02.c" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Struct: One New Field
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="cm"&gt;/* Heap buffer holding the elements              */&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt;  &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="cm"&gt;/* Elements currently stored                     */&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt;  &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="cm"&gt;/* Slots allocated                               */&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt;  &lt;span class="n"&gt;realloc_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* How many times we've reallocated (diagnostic) */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;IntArray&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We add &lt;code&gt;realloc_count&lt;/code&gt;,  a diagnostic counter that tracks how many times the buffer has been reallocated. This has no functional purpose; it exists so we can observe and discuss growth behavior. In a production library you'd likely omit it. For learning, it's invaluable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Star: array_push with Automatic Growth
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;array_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"array_push: NULL array&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cm"&gt;/* ── Do we need to grow? ──────────────────────────────────── */&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;old_cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;new_cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;old_cap&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="cm"&gt;/*
         * realloc() does one of two things:
         *   1. Extends the block in-place (returns same pointer).
         *   2. Allocates new block, copies data, frees old block
         *      (returns NEW pointer — old pointer is INVALID).
         *
         * We MUST use a temporary variable.
         */&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;realloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_cap&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* arr-&amp;gt;data still points to the original buffer */&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_cap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;realloc_count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cm"&gt;/* ── Normal push (guaranteed to have room now) ────────────── */&lt;/span&gt;
    &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the entire growth mechanism. When &lt;code&gt;size &amp;gt;= capacity&lt;/code&gt;, we double the capacity, call &lt;code&gt;realloc&lt;/code&gt;, and update the pointer. The rest of the function is identical to &lt;a href="https://pablogs.dev/posts/post-01-hello-array/" rel="noopener noreferrer"&gt;Post 1&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Compile and run the complete file to see it in action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcc &lt;span class="nt"&gt;-Wall&lt;/span&gt; &lt;span class="nt"&gt;-Wextra&lt;/span&gt; &lt;span class="nt"&gt;-Wpedantic&lt;/span&gt; &lt;span class="nt"&gt;-std&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;c11 &lt;span class="nt"&gt;-o&lt;/span&gt; post_02 post_02.c
./post_02
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starting from capacity=2, we push 12 elements and watch the buffer grow through three reallocations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  cap=2 → push #3 triggers realloc → cap=4
  cap=4 → push #5 triggers realloc → cap=8
  cap=8 → push #9 triggers realloc → cap=16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three reallocations for 12 elements. The doubling strategy means we reallocate less and less frequently as the array grows, that's the amortized O(1) property we'll analyze formally in Post 3.&lt;/p&gt;

&lt;p&gt;Here's the ASCII visualization after the third realloc, with 9 elements in a 16-slot buffer:&lt;/p&gt;

&lt;pre&gt;╔══════════════════════════════════════════════════════════╗
║  After push(90) — REALLOC: 8 → 16                        ║
╠══════════════════════════════════════════════════════════╣
║  size = 9      capacity = 16     elem = 4 bytes          ║
║  data = 0x55a3c0        (heap)                           ║
║  reallocations so far: 3                                 ║
╠══════════════════════════════════════════════════════════╣
║  ┌──────┌──────┌──────┌──────┌──────┌──────┌──────...    ║
║  │   10 │   20 │   30 │   40 │   50 │   60 │   70 ...    ║
║  └──────└──────└──────└──────└──────└──────└──────...    ║
║     0      1      2      3      4      5      6   ...    ║
╠══════════════════════════════════════════════════════════╣
║  36B used /  64B alloc =  56.2% utilization              ║
║  28B wasted ( 43.8%)     next realloc at size=16         ║
╚══════════════════════════════════════════════════════════╝
&lt;/pre&gt;

&lt;p&gt;Nine slots occupied, seven slots empty (shown as &lt;code&gt;·&lt;/code&gt; in the full output). The stats show 43.8% waste — that's the price of pre-allocating for future growth. Whether that's acceptable depends on your use case. Post 3 explores this tradeoff in depth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj1a6vc4jztxy9dxy1rqj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj1a6vc4jztxy9dxy1rqj.png" alt="Diagram showing the old buffer (red, dashed, "&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Diagram showing the old buffer (red, dashed, "Before, FREED") and the new buffer (green, solid, "After, CURRENT") with a dashed blue arrow labeled "memcpy + free old" connecting them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The diagram above shows what realloc does when it moves the buffer. The old allocation (red, dashed border) is freed after the data is copied to the new, larger allocation (green). Every pointer that referred to the old address is now dangling.&lt;/p&gt;
&lt;h2&gt;
  
  
  Walking Through the Code
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Temporary Pointer: Why It Matters
&lt;/h3&gt;

&lt;p&gt;The single most important detail in this post is three lines long:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;realloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_cap&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the pattern that seems equivalent but is actually a bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* ⚠ WRONG — DO NOT DO THIS */&lt;/span&gt;
&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;realloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_cap&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If realloc succeeds, both patterns produce the same result. But if realloc &lt;em&gt;fails&lt;/em&gt;, the consequences are completely different.&lt;/p&gt;

&lt;p&gt;With the wrong pattern: &lt;code&gt;realloc&lt;/code&gt; returns &lt;code&gt;NULL&lt;/code&gt;, which is assigned directly to &lt;code&gt;arr-&amp;gt;data&lt;/code&gt;. Now &lt;code&gt;arr-&amp;gt;data&lt;/code&gt; is &lt;code&gt;NULL&lt;/code&gt;. The old buffer, the one with all your data in it, is still allocated somewhere on the heap, but no pointer references it anymore. It is leaked. Your data is lost &lt;em&gt;and&lt;/em&gt; your memory is leaked. This is a double failure: data loss plus resource leak.&lt;/p&gt;

&lt;p&gt;With the correct pattern: &lt;code&gt;realloc&lt;/code&gt; returns &lt;code&gt;NULL&lt;/code&gt;, which is stored in &lt;code&gt;tmp&lt;/code&gt;. The check &lt;code&gt;if (!tmp)&lt;/code&gt; triggers and the function returns -1. Critically, &lt;code&gt;arr-&amp;gt;data&lt;/code&gt; was never touched, it still points to the original buffer with all the original data intact. The caller can handle the error (log it, free the array, try again with less memory) without losing anything.&lt;/p&gt;

&lt;p&gt;This is not a hypothetical concern. In embedded systems or long-running servers, allocation failures happen. The temporary pointer pattern is the difference between an array that degrades gracefully and one that silently corrupts your program state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pointer Invalidation: The Realloc Trap
&lt;/h3&gt;

&lt;p&gt;When realloc moves the buffer, every pointer into the old buffer becomes a dangling pointer. This is the most dangerous consequence of automatic growth, and it catches even experienced C programmers. Here's the scenario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;array_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;array_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="cm"&gt;/* Take a pointer into the buffer */&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  &lt;span class="cm"&gt;/* ptr points to the 100 */&lt;/span&gt;

&lt;span class="cm"&gt;/* This push triggers realloc (capacity 2 → 4) */&lt;/span&gt;
&lt;span class="n"&gt;array_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="cm"&gt;/* ptr now points to FREED MEMORY */&lt;/span&gt;
&lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="cm"&gt;/* Undefined behavior */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the third push, the buffer at &lt;code&gt;arr-&amp;gt;data&lt;/code&gt; may have moved to a new address. The variable &lt;code&gt;ptr&lt;/code&gt; still holds the old address. That memory has been freed by realloc, it might be reused by the next &lt;code&gt;malloc&lt;/code&gt;, overwritten with heap metadata, or still contain the old value by coincidence. Reading through &lt;code&gt;ptr&lt;/code&gt; is undefined behavior. It might print 100. It might print garbage. It might crash. The outcome depends on what the allocator did with the freed block, and that's not something your program controls.&lt;/p&gt;

&lt;p&gt;The rule is simple: &lt;strong&gt;after any operation that might trigger realloc (push, insert, resize), all pointers and references into the array's data buffer are potentially invalid.&lt;/strong&gt; If you need a stable reference to an element, store its index, not a pointer. Indices survive reallocation; pointers don't.&lt;/p&gt;

&lt;p&gt;The full source file includes a self-contained demo that creates this exact scenario and prints the pointer addresses before and after realloc so you can see the invalidation happen. Run it yourself, there's no substitute for watching the addresses change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Growth by Doubling
&lt;/h3&gt;

&lt;p&gt;We chose &lt;code&gt;new_cap = old_cap * 2&lt;/code&gt;, double the capacity on every realloc. This is the simplest growth strategy and the one most implementations start with. Starting from capacity 2, the progression is: 2 → 4 → 8 → 16 → 32 → 64 → ... Each time we hit the wall, we double.&lt;/p&gt;

&lt;p&gt;The key property: to reach size N starting from capacity 1, you need about log₂(N) reallocations. For a million elements, that's roughly 20 reallocations total. Each reallocation copies all existing elements, so the total work across all copies is bounded, the amortized cost per push is O(1). Post 3 will prove this formally and explore why some implementations prefer 1.5x growth over 2x.&lt;/p&gt;

&lt;p&gt;For now, notice what happens to waste. Right after a realloc, the buffer is about half empty (we just doubled, and only one new element was added). As we fill it up, utilization climbs toward 100%, and then we double again. The sawtooth pattern, waste spikes after realloc, then decreases with each push, is characteristic of geometric growth strategies. You can see it clearly in the ASCII output by watching the utilization percentage after each push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Concepts and Tradeoffs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  realloc Moves vs Extends: You Can't Choose
&lt;/h3&gt;

&lt;p&gt;Whether realloc extends in-place or moves to a new location is entirely up to the allocator. You might observe in-place extension during testing (especially with small arrays early in a program's life, when the heap is mostly empty) and then hit moves in production when the heap is fragmented.&lt;/p&gt;

&lt;p&gt;This is why correctness requires handling both cases identically. The temporary pointer pattern does this naturally: whether &lt;code&gt;tmp&lt;/code&gt; equals &lt;code&gt;arr-&amp;gt;data&lt;/code&gt; (in-place) or differs (moved), the assignment &lt;code&gt;arr-&amp;gt;data = tmp&lt;/code&gt; is correct either way. You don't need to check which case occurred, the pattern is correct for both.&lt;/p&gt;

&lt;p&gt;One thing you should &lt;em&gt;not&lt;/em&gt; do is rely on in-place extension for performance. Some codebases try to "help" the allocator by freeing and re-mallocing at a specific alignment. Unless you're writing the allocator itself, let realloc do its job. It knows more about the heap layout than you do.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cost of Growth
&lt;/h3&gt;

&lt;p&gt;Reallocation is expensive. It's O(n) where n is the number of existing elements, every byte must be copied. With 2x growth, the cost per realloc increases as the array gets larger: copying 10 elements is cheap, copying 10 million elements is not.&lt;/p&gt;

&lt;p&gt;The saving grace is that reallocations happen exponentially less often. You pay a big cost once, then enjoy cheap pushes until the next realloc. This amortization is what makes geometric growth viable. But it does mean that individual push operations have &lt;em&gt;unpredictable&lt;/em&gt; latency: most are O(1), but occasionally one is O(n). For real-time systems where you need bounded worst-case latency, this can be a problem, a topic we'll revisit in Post 12 on benchmarking.&lt;/p&gt;

&lt;h3&gt;
  
  
  When Capacity Grows, Memory Waste Spikes
&lt;/h3&gt;

&lt;p&gt;Right after doubling from capacity 8 to 16, you have 9 elements in 16 slots, 43.8% waste. That's 28 bytes of allocated-but-unused memory. For a small array, this is nothing. For an array of 10 million structs at 64 bytes each, doubling means allocating 640 MB when you only need 320 MB. The extra 320 MB might be the difference between fitting in RAM and hitting swap.&lt;/p&gt;

&lt;p&gt;The growth factor directly controls this tradeoff: 2x wastes more but reallocates less, 1.5x wastes less but reallocates more. Additive growth (say, adding 1024 slots each time) keeps waste bounded but destroys the amortized O(1) property. Post 3 is dedicated entirely to this debate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try This and Watch It Fail
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Experiment 1: The Dangerous Pattern.&lt;/strong&gt; Modify &lt;code&gt;array_push&lt;/code&gt; to use the direct assignment pattern: &lt;code&gt;arr-&amp;gt;data = realloc(arr-&amp;gt;data, new_cap * sizeof(int));&lt;/code&gt;. Remove the &lt;code&gt;tmp&lt;/code&gt; variable and the NULL check. Now simulate an allocation failure (you can do this on Linux with &lt;code&gt;LD_PRELOAD&lt;/code&gt; and a library that makes malloc/realloc fail after N calls, or simply replace the realloc line with &lt;code&gt;int *tmp = NULL;&lt;/code&gt; to simulate failure). Watch the array lose its data pointer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment 2: Pointer Invalidation in the Wild.&lt;/strong&gt; Create an array with capacity 1. Push one element. Take a pointer: &lt;code&gt;int *p = &amp;amp;arr-&amp;gt;data[0];&lt;/code&gt;. Now push a second element (this triggers realloc from 1 → 2). Print &lt;code&gt;*p&lt;/code&gt;. On many systems it will still print the old value, the freed memory hasn't been overwritten yet. Now push 1000 more elements. Print &lt;code&gt;*p&lt;/code&gt; again. The memory at the old address has likely been reused, and &lt;code&gt;*p&lt;/code&gt; will be garbage or crash. Compile with &lt;code&gt;-fsanitize=address&lt;/code&gt; to see AddressSanitizer catch the use-after-free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment 3: Counting Reallocations.&lt;/strong&gt; Modify &lt;code&gt;array_create&lt;/code&gt; to start with &lt;code&gt;capacity = 1&lt;/code&gt;. Push 1000 elements. How many reallocations happen? (Answer: about 10, because log₂(1000) ≈ 10.) Now change the growth strategy to &lt;code&gt;new_cap = old_cap + 10&lt;/code&gt; (additive growth). Push 1000 elements again. How many reallocations now? (Answer: about 100.) Feel the difference in efficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowledge Test
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you hold a pointer to &lt;code&gt;arr-&amp;gt;data[3]&lt;/code&gt; and then push triggers realloc, is your pointer still valid? Why?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No. When realloc moves the buffer to a new location, it frees the old buffer. Your pointer still holds the old address, which now points to freed memory. Dereferencing it is undefined behavior — you might read stale data, garbage, or crash. Even if realloc extends in-place (same address), you cannot &lt;em&gt;rely&lt;/em&gt; on that behavior, because you cannot predict which case will occur. The only safe approach is to treat all pointers into the buffer as potentially invalid after any operation that might trigger realloc. If you need a stable reference to an element, store the index and recompute the pointer: &lt;code&gt;arr-&amp;gt;data[3]&lt;/code&gt; will always be correct because &lt;code&gt;arr-&amp;gt;data&lt;/code&gt; is updated by the push function.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Our array grows automatically, but we made an arbitrary choice: double the capacity on every realloc. Why 2x and not 1.5x? Why not add a fixed amount each time? The answer turns out to be subtle, it involves amortized analysis, memory allocator behavior, and a surprising fact about when freed memory can be reused.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Post 3: "The Growth Factor Debate: 1.5x, 2x, or Something Else?"&lt;/strong&gt;, we'll put numbers on the tradeoffs. We'll calculate the amortized cost of push under different growth strategies, show why 2x growth can &lt;em&gt;never&lt;/em&gt; reuse previously freed memory but 1.5x can, and build a benchmark harness to measure the real-world difference. You'll come out of it able to justify your growth factor choice to anyone who asks.&lt;/p&gt;

&lt;p&gt;The array we built today is functionally complete for integers. It grows, it doesn't leak, and it handles allocation failures gracefully. But it only holds &lt;code&gt;int&lt;/code&gt;. In Post 4, we'll break that limitation with &lt;code&gt;void*&lt;/code&gt; and &lt;code&gt;memcpy&lt;/code&gt;, the C way of saying "I don't care what type you store, I'll hold it for you."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/ansuzgs/dynamic-arrays-c/blob/main/src/post_02.c" rel="noopener noreferrer"&gt;Full source code&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>c</category>
      <category>computerscience</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Hello, Array: malloc, free and Manual Bookkeeping</title>
      <dc:creator>Pablo GS</dc:creator>
      <pubDate>Fri, 29 May 2026 07:37:41 +0000</pubDate>
      <link>https://dev.to/pablogs/hello-array-malloc-free-and-manual-bookkeeping-4062</link>
      <guid>https://dev.to/pablogs/hello-array-malloc-free-and-manual-bookkeeping-4062</guid>
      <description>&lt;p&gt;&lt;em&gt;Post 1 of the Dynamic Arrays in C series · &lt;a href="https://github.com/ansuzgs/dynamic-arrays-c/blob/main/src/post_01.c" rel="noopener noreferrer"&gt;Full source code&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem No One Starts With
&lt;/h2&gt;

&lt;p&gt;You have five integers. You put them in an array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. C gives you a contiguous chunk of 20 bytes on the stack, indexed from 0 to 4, and life is good.&lt;/p&gt;

&lt;p&gt;Now your user wants to add a sixth integer. What do you do?&lt;/p&gt;

&lt;p&gt;You can't resize a stack array. Its size was baked into the binary at compile time, the compiler saw &lt;code&gt;5&lt;/code&gt;, calculated 20 bytes, and that's the space your function's stack frame has. There's no negotiation. You could declare &lt;code&gt;int numbers[1000]&lt;/code&gt; and hope it's big enough, but hope is not a memory management strategy.&lt;/p&gt;

&lt;p&gt;You could use a variable-length array (&lt;code&gt;int numbers[n]&lt;/code&gt;), but that just shifts the problem: &lt;code&gt;n&lt;/code&gt; is still fixed once you enter the function. Worse, VLAs live on the stack, which is limited to a few megabytes. Store a million integers and you blow the stack with no graceful recovery.&lt;/p&gt;

&lt;p&gt;The real solution lives on the heap. &lt;code&gt;malloc&lt;/code&gt; lets you ask the operating system for a chunk of memory at runtime, any size you want, limited only by available RAM. But malloc gives you raw bytes and a pointer. No size tracking. No bounds checking. No "how full am I?" bookkeeping. You get the memory and the responsibility.&lt;/p&gt;

&lt;p&gt;This is where every dynamic array begins: not with a clever data structure, but with a basic question of &lt;em&gt;bookkeeping&lt;/em&gt;. Who tracks how many elements you've stored? Who tracks how many you &lt;em&gt;could&lt;/em&gt; store? Who makes sure the memory gets freed when you're done?&lt;/p&gt;

&lt;p&gt;In C, the answer is always the same: you do.&lt;/p&gt;

&lt;p&gt;This post builds the simplest possible dynamic array, one that holds integers, has a fixed capacity, and does exactly three things: create, push, and destroy. No automatic growth (that's &lt;a href="https://pablogs.dev/posts/post-02-growing-pain/index.md" rel="noopener noreferrer"&gt;Post 2&lt;/a&gt;), no generics (Post 4), no error recovery (Post 6). Just the raw skeleton that everything else builds on.&lt;/p&gt;

&lt;p&gt;By the end, you'll understand two things most C tutorials skip: why the metadata struct exists, and why the order in which you call &lt;code&gt;free&lt;/code&gt; matters.&lt;/p&gt;

&lt;p&gt;Let's allocate some memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Struct: What an Array Knows About Itself
&lt;/h2&gt;

&lt;p&gt;A raw &lt;code&gt;malloc&lt;/code&gt; call returns &lt;code&gt;void *&lt;/code&gt;, a pointer to bytes with no meaning attached. If you allocate space for 10 integers, nobody remembers that number except you. The instant you lose track of the capacity, you're writing bugs.&lt;/p&gt;

&lt;p&gt;So the first thing a dynamic array needs isn't data. It's &lt;em&gt;metadata&lt;/em&gt;: a small struct that sits alongside the data and remembers the bookkeeping details.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="cm"&gt;/* Pointer to the heap buffer holding elements  */&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt;  &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="cm"&gt;/* How many elements have been stored           */&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt;  &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="cm"&gt;/* How many elements the buffer can hold        */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;IntArray&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three fields. This is the minimum viable bookkeeping:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;data&lt;/code&gt; is a pointer to the actual heap allocation where elements live. It's the result of a &lt;code&gt;malloc(capacity * sizeof(int))&lt;/code&gt; call. When you access &lt;code&gt;arr-&amp;gt;data[3]&lt;/code&gt;, you're reading the fourth integer in that allocation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;size&lt;/code&gt; tracks how many elements the user has actually pushed. It starts at 0 and increments with every &lt;code&gt;array_push&lt;/code&gt;. It is &lt;em&gt;not&lt;/em&gt; the same as capacity, this distinction is the single most important concept in dynamic array design.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;capacity&lt;/code&gt; tracks how many elements the allocation &lt;em&gt;can hold&lt;/em&gt;. If you malloced space for 10 integers, capacity is 10, even if size is only 3. The gap between size and capacity is wasted memory, we allocated it but aren't using it yet. Managing that gap is the art of dynamic arrays.&lt;/p&gt;

&lt;p&gt;Think of it like a parking garage. &lt;code&gt;capacity&lt;/code&gt; is the number of parking spots. &lt;code&gt;size&lt;/code&gt; is the number of cars currently parked. The garage exists at a specific address (&lt;code&gt;data&lt;/code&gt;). You can have an empty garage (size=0, capacity=100) or a full one (size=100, capacity=100), but you can never park more cars than spots, unless you build a bigger garage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwzxfedj199kvwuhg3t1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwzxfedj199kvwuhg3t1.png" alt="Diagram showing the IntArray metadata struct with a pointer to a contiguous heap buffer of 5 integer slots." width="799" height="150"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Diagram showing the IntArray metadata struct with a pointer to a contiguous heap buffer of 5 integer slots.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What malloc Actually Does
&lt;/h2&gt;

&lt;p&gt;Before looking at the implementation, it's worth understanding what happens when you call &lt;code&gt;malloc(20)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You're not asking the OS for exactly 20 bytes. The C runtime's allocator (glibc's &lt;code&gt;ptmalloc2&lt;/code&gt; on Linux, &lt;code&gt;jemalloc&lt;/code&gt; on FreeBSD, etc.) maintains pools of pre-allocated memory called &lt;em&gt;arenas&lt;/em&gt;. When you call &lt;code&gt;malloc(20)&lt;/code&gt;, the allocator finds a free block in one of its pools, marks it as used, and returns its address. If no pool has room, the allocator requests more memory from the kernel via &lt;code&gt;sbrk&lt;/code&gt; or &lt;code&gt;mmap&lt;/code&gt;, but this is the expensive path, and it happens rarely.&lt;/p&gt;

&lt;p&gt;The returned pointer has a hidden &lt;em&gt;header&lt;/em&gt; just before it (typically 8–16 bytes) where the allocator stores metadata: the block's size, whether it's in use, and pointers to adjacent free blocks. This is how &lt;code&gt;free&lt;/code&gt; knows how many bytes to release, you never tell it the size, because the allocator already recorded it.&lt;/p&gt;

&lt;pre&gt;What malloc returns:        What actually exists in memory:
                            ┌──────────────────┐
                            │ allocator header │  (hidden, 8-16 bytes)
    ptr ──────────────────► ├──────────────────┤
                            │                  │
                            │   your 20 bytes  │
                            │                  │
                            └──────────────────┘
&lt;/pre&gt;
 

&lt;p&gt;This has practical consequences. Every &lt;code&gt;malloc&lt;/code&gt; call costs not just the bytes you asked for, but also the overhead of that header. If you allocate a million 4-byte blocks, you're actually using 12–20 bytes per block, the 4 bytes you wanted plus the hidden header. For our dynamic array, this is why we make &lt;em&gt;two&lt;/em&gt; allocations (one for the struct, one for the buffer) instead of millions of individual &lt;code&gt;malloc(sizeof(int))&lt;/code&gt; calls: fewer allocations means less overhead.&lt;/p&gt;

&lt;p&gt;It also means that &lt;code&gt;free&lt;/code&gt; doesn't need to know the size of the allocation, it reads the header. But &lt;code&gt;free&lt;/code&gt; doesn't zero the memory or return it to the OS. The block is just marked as available in the allocator's free list. The bytes remain there, with their old values, until something else overwrites them. This is why use-after-free bugs are insidious: the data &lt;em&gt;looks&lt;/em&gt; valid long after you've freed it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;The implementation compiles with zero warnings under &lt;code&gt;gcc -Wall -Wextra -Wpedantic -std=c11&lt;/code&gt; and runs without leaks.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The full source, including a &lt;code&gt;main()&lt;/code&gt; that demonstrates every operation step by step, is available &lt;a href="https://github.com/ansuzgs/dynamic-arrays-c/blob/main/src/post_01.c" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;. Below are the essential pieces.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Lifecycle: Create and Destroy
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;array_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"array_create: capacity must be &amp;gt; 0&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntArray&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;       &lt;span class="cm"&gt;/* allocation #1 */&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"array_create: failed to allocate struct&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;     &lt;span class="cm"&gt;/* allocation #2 */&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"array_create: failed to allocate buffer&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="cm"&gt;/* Don't leak the struct if the buffer fails */&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Notice the error handling between the two allocations. If the first &lt;code&gt;malloc&lt;/code&gt; succeeds but the second fails, we have a partially-constructed object: a struct on the heap with an invalid &lt;code&gt;data&lt;/code&gt; pointer. If we returned NULL without freeing the struct, those 24 bytes (three fields on a 64-bit system: one pointer + two &lt;code&gt;size_t&lt;/code&gt;) would be leaked. The &lt;code&gt;free(arr)&lt;/code&gt; call before the &lt;code&gt;return NULL&lt;/code&gt; prevents that. This pattern, clean up everything you've allocated so far when a later step fails, is the foundation of resource cleanup in C. You'll see it scaled up in Post 6 when we discuss error handling strategies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;array_destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="cm"&gt;/* 1. free the element buffer                    */&lt;/span&gt;
    &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="cm"&gt;/*    (defensive: prevent use-after-free)        */&lt;/span&gt;
    &lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="cm"&gt;/* 2. free the struct itself                     */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two allocations in &lt;code&gt;array_create&lt;/code&gt;, two frees in &lt;code&gt;array_destroy&lt;/code&gt;. The symmetry is intentional and the order is not negotiable, more on that below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operations: Push and Get
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;array_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"array_push: NULL array&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"array_push: full (size=%zu, capacity=%zu). "&lt;/span&gt;
                &lt;span class="s"&gt;"Cannot add %d.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;array_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="nf"&gt;array_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;     &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="nf"&gt;array_capacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push writes to &lt;code&gt;arr-&amp;gt;data[arr-&amp;gt;size]&lt;/code&gt; and increments &lt;code&gt;size&lt;/code&gt;. Two lines of actual logic; the rest is validation. The expression &lt;code&gt;arr-&amp;gt;data[arr-&amp;gt;size]&lt;/code&gt; works because array indexing in C is pointer arithmetic: &lt;code&gt;arr-&amp;gt;data[n]&lt;/code&gt; is equivalent to &lt;code&gt;*(arr-&amp;gt;data + n)&lt;/code&gt;, which means "start at the address in &lt;code&gt;data&lt;/code&gt;, move forward &lt;code&gt;n * sizeof(int)&lt;/code&gt; bytes, and dereference." As long as &lt;code&gt;n &amp;lt; capacity&lt;/code&gt;, that address is within our allocation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;array_get&lt;/code&gt; returns the value through an output pointer (&lt;code&gt;out&lt;/code&gt;) instead of returning it directly. This is because we need two channels: the value itself and whether the operation succeeded. Returning &lt;code&gt;int&lt;/code&gt; for both the value and the error code would be ambiguous, is &lt;code&gt;-1&lt;/code&gt; an error or a legitimate stored value? The output pointer pattern separates these concerns cleanly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Driving It All
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;array_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="cm"&gt;/* This will fail — array is full */&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="cm"&gt;/* returns -1 */&lt;/span&gt;

    &lt;span class="cm"&gt;/* Read back */&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;array_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;array_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"arr[%zu] = %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;array_destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compile and run it yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcc &lt;span class="nt"&gt;-Wall&lt;/span&gt; &lt;span class="nt"&gt;-Wextra&lt;/span&gt; &lt;span class="nt"&gt;-Wpedantic&lt;/span&gt; &lt;span class="nt"&gt;-std&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;c11 &lt;span class="nt"&gt;-o&lt;/span&gt; post_01 post_01.c
./post_01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;pre&gt;╔══════════════════════════════════════════════════════╗
║  After push(30)                                      ║
╠══════════════════════════════════════════════════════╣
║  size = 3      capacity = 5      elem = 4 bytes      ║
║  data = 0x56fbb1d642d0  (heap)                       ║
╠══════════════════════════════════════════════════════╣
║  ┌──────┌──────┌──────┌──────┌──────┐                ║
║  │   10 │   20 │   30 │  ·   │  ·   │                ║
║  └──────└──────└──────└──────└──────┘                ║
║     0      1      2      3      4                    ║
║                   ▲ size=3                           ║
╠══════════════════════════════════════════════════════╣
║  12B used / 20B allocated = 60.0% utilization        ║
║  8B wasted (40.0%)                                   ║
╚══════════════════════════════════════════════════════╝
&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;Screenshot of the ASCII visualization output showing the array after pushing 3 elements (10, 20, 30) into a capacity-5 array, with occupied slots and empty slots clearly distinguished.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking Through the Code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Two Allocations, Two Frees
&lt;/h3&gt;

&lt;p&gt;The most important pattern in this file is the symmetry between &lt;code&gt;array_create&lt;/code&gt; and &lt;code&gt;array_destroy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;array_create&lt;/code&gt; performs two allocations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntArray&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;          &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;    &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;array_destroy&lt;/code&gt; performs two frees, in reverse order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;
&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reverse order is not a stylistic preference, it's a correctness requirement. Let's trace what happens if you swap them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* WRONG — undefined behavior */&lt;/span&gt;
&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="cm"&gt;/* struct memory returned to allocator */&lt;/span&gt;
&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="cm"&gt;/* arr is now a dangling pointer — reading arr-&amp;gt;data
                        is an invalid memory access */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;free(arr)&lt;/code&gt;, the memory at &lt;code&gt;arr&lt;/code&gt; has been returned to the allocator. It could be reused by the very next &lt;code&gt;malloc&lt;/code&gt; call, even one happening on another thread. Reading &lt;code&gt;arr-&amp;gt;data&lt;/code&gt; at that point might return the original pointer value (if the memory hasn't been touched yet), or it might return garbage (if the allocator has overwritten those bytes with its own bookkeeping data for the free list). Either way, it's undefined behavior. Valgrind would flag this as "Invalid read of size 8" (the size of a pointer).&lt;/p&gt;

&lt;p&gt;The general principle: when you have nested allocations (a struct that owns pointers to other allocations), free from the inside out, innermost allocations first, outermost last.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Defensive NULL Assignment
&lt;/h3&gt;

&lt;p&gt;After freeing the data buffer, we set &lt;code&gt;arr-&amp;gt;data = NULL&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="cm"&gt;/* defensive */&lt;/span&gt;
&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this simple code, nothing touches &lt;code&gt;arr-&amp;gt;data&lt;/code&gt; between the two frees, so the NULL assignment does nothing. But in more complex code, with error handlers, callbacks, or cleanup functions that might run between those two lines, the NULL acts as a safety net. If anything tries to dereference &lt;code&gt;arr-&amp;gt;data&lt;/code&gt; after the first free, it hits a NULL pointer dereference instead of a use-after-free. A NULL dereference is a loud, immediate crash with a clear stack trace. A use-after-free is a silent corruption that might not manifest until thousands of lines later. You trade one bug for a more debuggable bug.&lt;/p&gt;

&lt;p&gt;This pattern is sometimes called &lt;em&gt;defensive clearing&lt;/em&gt; or &lt;em&gt;poisoning&lt;/em&gt;. Some codebases go further and zero the entire struct before the final free (&lt;code&gt;memset(arr, 0, sizeof(*arr))&lt;/code&gt;), though that has a cost: the compiler might optimize it away if it can prove nothing reads the struct afterward (since reading freed memory is UB). Post 6 addresses this in the context of a broader error-handling strategy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pointer Arithmetic Inside Push
&lt;/h3&gt;

&lt;p&gt;The line that does the actual work in &lt;code&gt;array_push&lt;/code&gt; is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single line involves three dereferences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;arr&lt;/code&gt; is dereferenced to access the struct on the heap&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;arr-&amp;gt;data&lt;/code&gt; is dereferenced to get the base address of the buffer&lt;/li&gt;
&lt;li&gt;The result is indexed by &lt;code&gt;arr-&amp;gt;size&lt;/code&gt; to compute the write address
On a 64-bit system, the write address is calculated as:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;write_address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;size &amp;gt;= capacity&lt;/code&gt; check before this line guarantees that &lt;code&gt;write_address&lt;/code&gt; falls within our allocated buffer. Without it, we'd be writing past the end of our allocation, a heap buffer overflow. Depending on what lives past our allocation, this could corrupt the allocator's metadata (causing a crash in a later, unrelated &lt;code&gt;malloc&lt;/code&gt; or &lt;code&gt;free&lt;/code&gt;), overwrite another variable's data (causing impossible-looking bugs), or hit a guard page (causing an immediate segfault). AddressSanitizer (&lt;code&gt;gcc -fsanitize=address&lt;/code&gt;) catches these instantly, and it's worth compiling with it during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Concepts and Tradeoffs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stack vs Heap for the Metadata Struct
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;array_create&lt;/code&gt; allocates the &lt;code&gt;IntArray&lt;/code&gt; struct on the heap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntArray&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why not put it on the stack? You could write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="nf"&gt;array_create_stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IntArray&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* returns a copy */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, and it's actually slightly faster, stack allocation is just a pointer decrement on the stack pointer register, while &lt;code&gt;malloc&lt;/code&gt; navigates free lists. But it changes the API in subtle and dangerous ways.&lt;/p&gt;

&lt;p&gt;The caller receives a &lt;em&gt;copy&lt;/em&gt; of the struct. If they pass that copy to &lt;code&gt;array_push&lt;/code&gt;, the function modifies its &lt;em&gt;own&lt;/em&gt; copy of &lt;code&gt;size&lt;/code&gt;, the caller's &lt;code&gt;size&lt;/code&gt; remains unchanged. You'd need to pass by pointer everywhere: &lt;code&gt;array_push(&amp;amp;arr, value)&lt;/code&gt;. That's workable, but easy to forget, and the compiler won't warn you.&lt;/p&gt;

&lt;p&gt;More seriously, the stack struct's lifetime is tied to its scope. If you return it from a function, you're returning a copy (fine). If you store a pointer to it and the function returns, that pointer is dangling (catastrophic). Heap allocation gives you a stable pointer that survives function boundaries, can be stored in other data structures, and has a clear ownership model: whoever holds the pointer is responsible for calling &lt;code&gt;array_destroy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a library API, heap allocation is the standard choice. You'll see this pattern in virtually every C library: &lt;code&gt;thing_create()&lt;/code&gt; returns a pointer, &lt;code&gt;thing_destroy()&lt;/code&gt; frees it. The tradeoff is explicit: you trade a few nanoseconds of &lt;code&gt;malloc&lt;/code&gt; overhead and the burden of manual cleanup for an API that's unambiguous about ownership and lifetime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory Waste: The Capacity Problem
&lt;/h3&gt;

&lt;p&gt;When you create an array with capacity 10 and store 3 elements, you're wasting 7 slots × 4 bytes = 28 bytes. That's a 70% waste ratio. For our post, where we push 5 elements into a capacity-5 array, waste drops to 0% by the end, but there's a window where we're paying for memory we haven't used yet.&lt;/p&gt;

&lt;p&gt;Is this bad? It depends on scale. For a single array, 28 bytes is negligible. For a million small arrays in a memory-constrained embedded system, it might matter. For one large array, the waste percentage drops to near zero as you fill it.&lt;/p&gt;

&lt;p&gt;The real question is: &lt;em&gt;what capacity should you start with?&lt;/em&gt; If you pick too small (capacity=1), you'll need to reallocate on every push once we add growth in &lt;a href="https://pablogs.dev/posts/post-02-growing-pain/index.md" rel="noopener noreferrer"&gt;Post 2&lt;/a&gt;, each reallocation involves copying all existing elements. If you pick too large (capacity=10000), you waste memory on arrays that only hold 5 elements. The tension between &lt;em&gt;time&lt;/em&gt; (fewer reallocations) and &lt;em&gt;space&lt;/em&gt; (less waste) is the central tradeoff of dynamic arrays, and it leads directly to the growth factor analysis in Post 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Return Codes, Not Assertions
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;array_push&lt;/code&gt; returns 0 on success and -1 on failure. It doesn't abort the program. This is a conscious design decision: the &lt;em&gt;caller&lt;/em&gt; should decide what to do when a push fails. Maybe the caller wants to log and continue. Maybe they want to resize and retry. Maybe they want to exit. By returning an error code, we give the caller that choice.&lt;/p&gt;

&lt;p&gt;The alternative, &lt;code&gt;assert(arr-&amp;gt;size &amp;lt; arr-&amp;gt;capacity)&lt;/code&gt;, kills the program with no recovery. That's appropriate for programmer errors (invariants that should never be violated if the code is correct), but not for runtime conditions like "the array is full." A full array isn't a bug, it's a foreseeable state that the program should handle.&lt;/p&gt;

&lt;p&gt;There's a subtlety here about &lt;code&gt;errno&lt;/code&gt; and error reporting that's worth flagging. Our current approach, printing to stderr and returning -1, is fine for a learning exercise, but production code would typically set &lt;code&gt;errno&lt;/code&gt; or return a richer error type. We'll discuss the full spectrum of error handling strategies in Post 6.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pointer Ownership: The Contract
&lt;/h3&gt;

&lt;p&gt;There's an implicit contract in our API that no comment or type annotation enforces: the pointer returned by &lt;code&gt;array_create&lt;/code&gt; is &lt;em&gt;owned&lt;/em&gt; by the caller. Ownership means exactly one thing: the owner is responsible for calling &lt;code&gt;array_destroy&lt;/code&gt; on it. Nobody else should free it. Nobody should free parts of it (&lt;code&gt;arr-&amp;gt;data&lt;/code&gt;) independently. And once &lt;code&gt;array_destroy&lt;/code&gt; has been called, every copy of that pointer becomes invalid.&lt;/p&gt;

&lt;p&gt;C has no language-level mechanism to enforce this. Rust has &lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt; and affine types. C++ has &lt;code&gt;std::unique_ptr&lt;/code&gt;. In C, ownership is a convention, one you communicate through documentation, naming (&lt;code&gt;create&lt;/code&gt;/&lt;code&gt;destroy&lt;/code&gt; pairs), and discipline.&lt;/p&gt;

&lt;p&gt;The bugs that arise from violating ownership are among the worst in C:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Double free&lt;/strong&gt;: two parts of the code both think they own the pointer and both call &lt;code&gt;array_destroy&lt;/code&gt;. The second free corrupts the allocator's metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use after free&lt;/strong&gt;: one part of the code frees the pointer while another still holds a copy and keeps using it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory leak&lt;/strong&gt;: nobody frees the pointer because everyone assumes someone else will.
In this simple post, ownership is obvious, &lt;code&gt;main&lt;/code&gt; creates, &lt;code&gt;main&lt;/code&gt; destroys. In larger programs with shared data structures, callbacks, and multithreading, ownership becomes the hardest problem in C. We'll revisit this in Post 7 when we add function pointers and destructors for element types.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try This and Watch It Fail
&lt;/h2&gt;

&lt;p&gt;Before moving on, try these experiments with the code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment 1: The Memory Leak.&lt;/strong&gt; In &lt;code&gt;array_destroy&lt;/code&gt;, comment out &lt;code&gt;free(arr-&amp;gt;data)&lt;/code&gt;. Compile and run under valgrind (&lt;code&gt;valgrind --leak-check=full ./post_01&lt;/code&gt;). You'll see "definitely lost: 20 bytes", that's the orphaned buffer. The struct was freed, but the buffer it pointed to was not. This is &lt;em&gt;exactly&lt;/em&gt; the bug the knowledge test asks about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment 2: Use After Free.&lt;/strong&gt; In &lt;code&gt;main&lt;/code&gt;, add &lt;code&gt;printf("%d\n", arr-&amp;gt;data[0]);&lt;/code&gt; &lt;em&gt;after&lt;/em&gt; &lt;code&gt;array_destroy(arr)&lt;/code&gt;. Compile and run. It might print &lt;code&gt;10&lt;/code&gt;. It might print garbage. It might crash. That's undefined behavior, the data is freed, but the memory hasn't necessarily been overwritten yet. Valgrind would flag this as "Invalid read of size 4."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment 3: Buffer Overflow.&lt;/strong&gt; Remove the &lt;code&gt;size &amp;gt;= capacity&lt;/code&gt; check in &lt;code&gt;array_push&lt;/code&gt;. Push 100 elements into a capacity-5 array. Compile with AddressSanitizer (&lt;code&gt;gcc -fsanitize=address&lt;/code&gt;) and watch it detect the heap-buffer-overflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment 4: The Double Free.&lt;/strong&gt; In &lt;code&gt;main&lt;/code&gt;, call &lt;code&gt;array_destroy(arr)&lt;/code&gt; twice. On many systems the second call will crash with "double free or corruption." Some allocators detect this immediately; others corrupt silently and crash later. This is why our &lt;code&gt;array_destroy&lt;/code&gt; accepts NULL gracefully, if you set &lt;code&gt;arr = NULL&lt;/code&gt; after the first destroy, the second call becomes a no-op.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowledge Test
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happens if you call &lt;code&gt;free(arr)&lt;/code&gt; but forget to call &lt;code&gt;free(arr-&amp;gt;data)&lt;/code&gt; first?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The struct is returned to the heap allocator, but the buffer it pointed to, the &lt;code&gt;capacity * sizeof(int)&lt;/code&gt; bytes at &lt;code&gt;arr-&amp;gt;data&lt;/code&gt;, remains allocated. No pointer to it exists anymore (the struct that held the pointer is freed), so the memory is &lt;em&gt;leaked&lt;/em&gt;. It will never be freed for the rest of the program's lifetime. On a long-running program, repeated leaks like this accumulate and eventually exhaust memory. Valgrind would report "definitely lost: N bytes in 1 blocks."&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Our array works, but it has a crippling limitation: when it's full, push fails. The user has to guess the right capacity upfront, and if they guess wrong, they're stuck.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;&lt;a href="https://pablogs.dev/posts/post-02-growing-pain/index.md" rel="noopener noreferrer"&gt;Post 2&lt;/a&gt;: "Growing Pains: realloc and Automatic Capacity Management"&lt;/strong&gt;, we remove this limitation. We'll introduce &lt;code&gt;realloc&lt;/code&gt;, the call that says "give me more space, and copy my data to the new location if needed." You'll learn why old pointers become invalid after a realloc, why the growth factor matters (spoiler: it determines your amortized cost), and why you must &lt;em&gt;never&lt;/em&gt; write &lt;code&gt;arr-&amp;gt;data = realloc(arr-&amp;gt;data, new_size)&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;The fixed-capacity array you built today is the foundation. Everything from here builds on it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/ansuzgs/dynamic-arrays-c/blob/main/src/post_01.c" rel="noopener noreferrer"&gt;Full source code&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>c</category>
      <category>computerscience</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
