{"article":{"slug":"why-c-strings-suck-and-how-i-fixed-them","title":"Why C Strings Suck And How I Fixed Them","subtitle":null,"summary":"Elias Ebner explains how NUL-terminated strings work in C and why they cause bugs and inefficiency, then walks through a small single-file open-source C library built around length-carrying string view and owned string types, with design decisions, API examples and a note on unity builds.","content_type":"tutorial","language":"en","canonical_url":"https://eliasebner.com/blog/guides/fixing-strings-in-c/","author":{"name":"Elias Ebner","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"eliasebner.com","url":"https://eliasebner.com/","listing_slug":null,"listing":null},"topics":[{"name":"C","slug":"c","url":"https://listedarticles.com/topics/c"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"},{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":3770,"reading_minutes":16,"published_at":"2026-09-07T00:00:00.000Z","added_at":"2026-10-15T02:12:13.356Z","updated_at":"2026-10-15T02:12:13.356Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/why-c-strings-suck-and-how-i-fixed-them","markdown_url":"https://listedarticles.com/articles/why-c-strings-suck-and-how-i-fixed-them.md","example":false,"citation":"Elias Ebner, eliasebner.com. \"Why C Strings Suck And How I Fixed Them.\" 7 Sept 2026. https://eliasebner.com/blog/guides/fixing-strings-in-c/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://eliasebner.com/blog/guides/fixing-strings-in-c/"},"body_markdown":"I have always been frustrated about C not having a proper `String` type that most modern\nlanguages have nowadays. I love the simplicity of C, but there are some features that I\nwould really like to have in it, and this is one of them.\n\nSince I made a small library (just one `.c` file) that you can use to make the experience\nof working with strings in C just a little better, I would like to show it to you here.\n\nFor the impatient, this is the general idea:\n\n```\nstruct str_view {\n    const u8       *data;\n    usize           len;\n};\nstruct str_owned {\n    u8             *data;\n    usize           len;\n};\n```\nBut before getting into the library itself, I will go over how “strings” work in C and why I find them annoying, what the issues are with this model, and then it will be easier to explain why I decided to take the route that I took.\n\nIf you only care about the actual implementation details,\nyou can [click here](#the-solution).\n\n## How Strings Work in C\n\nAs many might already know, strings in C are NUL-terminated. NUL is an ASCII character\nwhich, in the context of ASCII encoding, literally corresponds to the value 0.\nIn C, this character is represented by `'\\0'`. Inside a text, it generally does not\ncarry any semantic meaning, so it will never be used inside the text for the sake\nof carrying information (as opposed to letters like `'a'` or `'y'`).\n\nC does not really have “strings” per se. It only has arrays. These arrays are simply chunks of data where you can put things. To understand where the array ends, one has two options:\n\n- Store the length of the data somewhere\n- Put something at the end of the array to signify its end\n\nC went with the second option.\n\nSince “strings” (which are not really strings) in C are simply arrays of characters, each string has a NUL-terminator at the end of it.\n\nC is a very old language and back then - when it was first created - this seemed like a good idea.\n\nStoring the length of the data separately required more memory than a NUL-character at the end. A NUL-character in ASCII encoding is 8 bits (or 1 byte). If you tried to store the length of the data in only 8 bits, you could only have strings that are up to 255 characters in length, which is obviously too short. Bigger numbers require more data to store, so we’re talking about around 4 to 8 bytes instead of 1.\n\nBut this is not even the main reason - the main reason is because of simplicity. Back when C was created, there existed some languages that already had strings with a separately stored length. C just wanted to be as simple as possible, and having NUL-terminated character arrays seemed like a sane decision.\n\n## The Problems With This Approach\n\nThere are multiple issues with this.\n\n### First Issue\n\nFirst of all, if you want to find out the length of a string, with a NUL-terminated string\nyou have to start at index `0` and start counting until you reach the end of the data.\nThis process has a time complexity of , as opposed to  for just reading the\nseparately stored length.\n\nThis is fine for short character arrays, but imagine you had to figure out the length of a character text. That would take a considerable amount of CPU cycles that could have been used for something else.\n\nThis bothers me especially in a language like C, since it is supposed to give you some of the best performance that programming languages have to offer (without counting assembly and certain exceptions in some specific scenarios). C is often the baseline when comparing languages to gauge their relative performance. Why, if C focuses so much on performance, would you settle for this?\n\n### Second Issue\n\nLet us assume that we have some C-string `some_str`:\n\n`char *some_str = \"Hello World\";`\nThis is great, but now I would like to work with the second word specifically (`World`).\nI do not care about changing the data, just about reading. Maybe I want to pass it to a\nfunction or something.\nWell, I could do something like this:\n\n`char *only_world = some_str + 6;`\nBy doing this, we are reusing the NUL-terminator that `some_str` was using to terminate\nitself.\n\nFine, what if I want to access the first word only, though? Now we have a problem.\nWe somehow have to end the string at the fifth index. To do that, we would have to\ninsert a NUL-character at index `6`. By doing so, we are changing the data that `some_str`\nis pointing to, and we can therefore not use that variable anymore.\n\nSo, what do we do? We have to copy the string. That’s slow, since it involves memory lookups. We have to allocate to memory, and then copy it. We can also use the stack if we know the size of the data at compile time, but that is often not the case. That would look like this:\n\n```\nchar *only_hello = malloc(6);\nmemcpy(only_hello, some_str, 5);\nonly_hello[5] = 0;\n```\nIn this simple example, the stack would have been fine, but in most realistic scenarios, we have to use heap allocations.\n\nThis problem is solved by so-called “string views”, which are basically immutable data and a\nlength. You can just give the string view the same pointer as `some_str` and decrease the\nlength to whatever you need, `5` in this case.\n\n### Third Issue\n\nGod forbid you forget to NUL-terminate a string. If you pass a pointer to a non-NUL-terminated\nstring to a function in `string.h`, like `strlen`, the computer will just keep counting\ncharacters until it either randomly finds a NUL-character or - alternatively - until it\ntries to access a region of memory that it should not touch and the operating system\nkills the process.\n\nThis generally does not happen with actual strings (or string views), since you will use functions to interact with them, and they always keep the length of the string in sync with the data. In that case you always know when the string stops, you do not have to rely on remembering to NUL-terminate all your strings.\n\n### Other Issues\n\nThere are probably more issues, but these are hopefully enough to convince you. In general, I find strings with a separately stored length more intuitive and easier to work with.\n\n## The Solution\n\nLuckily, strings are not particularly hard to implement in C. Here is how I did it.\n\n### Types\n\nJust so you are aware when reading the code, I `typedef` some basic types in C before I\nstart coding. In this case we will only need a few of them, but this is what I paste at\nthe start of every project of mine.\n\n```\n#include <stdint.h>\n#include <stdbool.h>\n#include <stddef.h>\ntypedef uint8_t     u8;\ntypedef uint16_t    u16;\ntypedef uint32_t    u32;\ntypedef uint64_t    u64;\ntypedef int8_t      i8;\ntypedef int16_t     i16;\ntypedef int32_t     i32;\ntypedef int64_t     i64;\ntypedef float       f32;\ntypedef double      f64;\ntypedef size_t      usize;\n```\nThis makes it more convenient to use the integer types with a fixed size instead\nof the older `int`, `long`, `long long`, etc.\n\nI generally prefer this since `int` is not the same size on all platforms.\n`int32_t` on the other hand is guaranteed to be exactly 32 bits.\n\n### Arena Allocator\n\nFor your information, I have adapted this library to be compatible with my arena implementation.\nIt can be found in the same repository as this string library. I have an article about it\n[here](/blog/guides/arena-allocators-in-c/) if you would like to read up on that.\n\nWhat you need to know is that each function that needs to allocate some memory receives\na `struct arena *` as its first argument. Instead of `malloc` and `realloc` I use\n`arena_alloc` and `arena_realloc` respectively.\n\n### Structs\n\nFirst, let’s define our string types. I don’t prefer not to `typedef` structs, but\nif you want to you can do so. If you do decide to `typedef` these structs,\nI would advise you to use `PascalCase` as a naming convention (to better differentiate them\nfrom built-in types).\n\n```\nstruct str_view {\n    const u8       *data;\n    usize           len;\n};\nstruct str_owned {\n    u8             *data;\n    usize           len;\n};\n```\nAs you can see, we have two string types: `struct str_view` and `struct str_owned`.\nThe difference is that `struct str_owned` owns the data, and the data can also be\nchanged. Ideally, you should try to only have one `str_owned` pointing at any given\nregion in memory.\n\nOn the other hand, `struct str_view` should be used as read-only views of a\ngiven region of data. It should not be used to modify the data.\n\nSeparating these two concerns is nice for multiple reasons. Who owns the data becomes clear immediately. This is especially useful when returning string structs from functions. By using this model, it is easier to understand whether we need to concern ourselves with freeing data or something similar.\n\nAs a general rule of thumb, `struct str_owned` are heap-allocated.\nIf you have a `struct str_view` viewing some region in memory owned by\na `struct str_owned`, that string view is only valid as long as the underlying\nowned string is still valid. That means that you have to be careful about\nnot freeing the `struct str_owned` and after that still using string views\nthat view that data.\n\nAlso, you might have noticed that I am using `u8`s (a.k.a. `uint8_t`s) instead of\n`char`s. That is because, technically - even though in almost all cases it does not matter -\n`char`s are not guaranteed to have a size of exactly 8 bits.\nOn certain embedded systems, a `char` is sometimes 16 or 32 bits large.\nOn very old systems, they used to be 9 bits. This has historical reasons.\nA `char` is simply guaranteed to have a size of one byte, but the exact size of a byte\nis not clearly defined as a definitive size in bits. Knowing all that, using `u8` is more\nconsistent across platforms.\n\nAs a side note, this is also the reason why the `CHAR_BIT` macro exists. It tells you\nhow many bits are in a `char` on the system that the code is run. That is because the\nsize of a `char` depends on the system that you are currently on.\n\n\nAlso, I do not really like using a redundant type. A `u8` holds all the semantic\ninformation I need, so `u8` it is.\n\n### Macros\n\nNext, we create some macros. I try to minimize the use of macros, but in certain cases they are just perfect for the job. I believe that this is one of them.\n\nMainly I care about the `STR` macro, that I use to easily create a `struct str_view`\nout of a `char *` literal.\n\n```\n#define LENGTHOF(s) (sizeof(s) / sizeof(*s))\n#define STR(s) (struct str_view){(const u8 *)s, LENGTHOF(s) - 1}\n#define STR_PRINT(s) (i32) s.len, s.data\n```\n`STR_PRINT` is used to print a `struct str_view`. You can use the `%.*s` format specifier\nto manually give it the length of the string to be printed (since string views\nare not guaranteed to be NUL-terminated, since they can view a region of memory\nin the middle of a `struct str_owned`). This can be done as follows:\n\n`printf(\"Here is my string: %.*s\\n\", STR_PRINT(STR(\"some_string\")));`\nHere you can also see the `STR` macro in action, which creates a `struct str_view` on the fly\nfrom a `char *` literal.\n\n### Freeing Memory\n\nSince my implementation uses memory arenas, there is no need to free an owned string\nspecifically, but if you would like to move away from arenas you can always create\na simple wrapper function that calls `free` on the `data` field of an owned string.\n\n### Conversion Functions\n\nWe need some way to convert between `struct str_view` and `struct str_owned` and back.\nConverting an owned string to a string view is trivial; you simply use the same pointer\nand length of the owned string in a `struct str_view`:\n\n```\n[[nodiscard]]\nstruct str_view str_owned_to_str_view(struct str_owned s) {\n    struct str_view str_out = {0};\n    str_out.data = s.data;\n    str_out.len = s.len;\n    return str_out;\n}\n```\nAlso, as you might have noticed, I like using `[[nodiscard]]`. This is a compiler\ndirective which makes the compiler issue a warning if the return value of the function\nflagged with this directive is disregarded.\nIn scenarios like this one I find it to be very useful, since it is completely useless\nto call this function if you are not going to do anything with the result.\n\nWe also need to convert string views to owned strings. This is a bit more tricky and should not be done unless strictly necessary, since it requires memory allocation. An owned string can be mutated, which means that we have to give it its own region in memory to avoid damaging other owned strings that might be using the same memory (recall when I said that we should try to avoid having multiple owned strings on the same chunk of memory).\n\n```\nstruct str_view_to_str_owned_out {\n    struct str_owned str;\n    bool success;\n};\n[[nodiscard]]\nstruct str_view_to_str_owned_out str_view_to_str_owned(\n    struct arena *arena,\n    struct str_view s\n) {\n    struct str_view_to_str_owned_out out = {0};\n    struct arena_alloc_out alloc_out = arena_alloc(arena, s.len + 1);\n    if (!alloc_out.success)\n        return out;\n    out.str.data = alloc_out.ptr;\n    memcpy(out.str.data, s.data, s.len);\n    out.str.len = s.len;\n    out.str.data[out.str.len] = 0;\n    out.success = true;\n    return out;\n}\n```\nAs a small side note, whenever I need to return multiple values from a function I don’t like using output parameters (which is a technique often used in C programs). I prefer to return a struct. Also, it does not allow the caller of the function to use the return value directly, which makes it more likely that the programmer will remember to check for errors.\n\nIt makes code very verbose but more consistent in my opinion, as opposed to output parameters. If you don’t like it, moving away from this system and adapting the code to your own needs should be trivial.\n\n### Utility Functions\n\nI also wrote a bunch of functions that implement common string operations and hopefully make the experience of working with strings much more ergonomic than the standard library.\n\n#### Checking Whether a String Starts With a Substring\n\n```\n[[nodiscard]]\nbool str_starts_with(struct str_view s, struct str_view start) {\n    if (s.len < start.len)\n        return false;\n    return 0 == memcmp(s.data, start.data, start.len);\n}\n```\n#### Slicing Strings\n\nThis should behave similarly to the `String.slice` function in JavaScript.\n\n```\nstruct str_slice {\n    i32 start;\n    bool has_start;\n    i32 end;\n    bool has_end;\n};\nstruct str_view str_slice(struct str_view s, struct str_slice slice) {\n    struct str_view out = {0};\n    if (slice.has_start) {\n        if (slice.start < 0) {\n            slice.start = s.len + slice.start;\n        }\n    } else {\n        slice.start = 0;\n    }\n    if (slice.has_end) {\n        if (slice.end < 0) {\n            slice.end = s.len + slice.end;\n        }\n    } else {\n        slice.end = s.len;\n    }\n    out.data = s.data + slice.start;\n    i32 out_len = slice.end - slice.start;\n    out.len = out_len >= 0 ? out_len : 0;\n    return out;\n}\n```\nI decided to use a `struct` for the input parameter, because this makes it\neasier to support optional arguments. You can simply initialize it like this\n\n`struct str_slice slice = {0};`\nAnd then, by default, both boolean fields are false. You can then set whatever fields\nyou want. If you do not provide a `start`, the default is `0`. If you do not\nprovide an `end`, the default is the length of the string view.\n\n`start` is inclusive, while `end` is exclusive.\nI could not use negative values for representing “unset” values. For one, I really dislike\nthis practice, and also I wanted to give the user the option of passing negative\nvalues which reference the end of the string.\n\nWhat I mean is that if you pass `-1` as `start`, the function will start slicing at\nthe last character of the string. `-2` would be second to last.\n\nThis is really ergonomic whenever you only want to shave off some characters at the end\n(for example, the `\\r\\n\\r\\n` at the end of the head of an HTTP string).\n\n#### Finding a Substring in a String\n\nI loop keep slicing away one character at a time from a string and check if the resulting string starts with the desired substring.\n\n```\nstruct str_find_substr_out {\n    usize idx;\n    bool found;\n};\n[[nodiscard]]\nstruct str_find_substr_out str_find_substr(\n    struct str_view s,\n    struct str_view sub_s\n) {\n    struct str_find_substr_out out = {0};\n    if (s.len < sub_s.len)\n        return out;\n    usize match_index = 0;\n    for (usize i = 0; i < s.len; i++) {\n        struct str_slice slice = {0};\n        slice.has_start = true;\n        slice.start = i;\n        struct str_view s_slice = str_slice(s, slice);\n        if (str_starts_with(s_slice, sub_s)) {\n            out.found = true;\n            out.idx = i;\n            return out;\n        }\n    }\n    return out;\n}\n```\n#### Determining Whether a String Contains a Substring\n\nFor this, I simply use the `str_find_substr` function defined above and extract the `found`\nfield from the `struct str_find_out` output.\n\n```\n[[nodiscard]]\nbool str_contains_substr(struct str_view s, struct str_view sub_s) {\n    return str_find_substr(s, sub_s).found;\n}\n```\n#### Determining Whether Two Strings Are Equal\n\nThis is straight forward. Check the lengths and compare the memory.\n\n```\n[[nodiscard]]\nbool str_eq(struct str_view s1, struct str_view s2) {\n    if (s1.len != s2.len)\n        return false;\n    return 0 == memcmp(s1.data, s2.data, s1.len);\n}\n```\nHere is the same thing, but it ignores the casing. Adding `32` to an uppercase letter\ncharacter converts it to the lowercase equivalent. This, of course, only works\nwith ASCII encodings.\n\n```\n[[nodiscard]]\nbool str_eq_ignore_case(struct str_view s1, struct str_view s2) {\n    if (s1.len != s2.len)\n        return false;\n    for (usize i = 0; i < s1.len; i++) {\n        u8 c1 = s1.data[i];\n        u8 c2 = s2.data[i];\n        if (c1 >= 65 && c1 <= 90)\n            c1 += 32;\n        if (c2 >= 65 && c2 <= 90)\n            c2 += 32;\n        if (c1 != c2)\n            return false;\n    }\n    return true;\n}\n```\n#### Splitting Strings\n\nThis one is a bit more complicated, since it involves heap allocations. At first, I implemented it in a way that required a memory allocation for each new split that was found. I later changed it and created this helper function:\n\n```\nu32 str_count_substr(struct str_view s, struct str_view sub_s) {\n    u32 out = 0;\n    usize sub_s_index = 0;\n    for (usize i = 0; i < s.len; i++) {\n        struct str_slice slice = {0};\n        slice.has_start = true;\n        slice.start = i;\n        struct str_view sliced_s = str_slice(s, slice);\n        if (str_starts_with(sliced_s, sub_s))\n            out++;\n    }\n    return out;\n}\n```\nThis counts the number of occurrences of a substring before splitting, which is useful for allocating all the required memory upfront. The number of splits will be the number of occurrences of the delimiter plus one.\n\nThe actual splitting happens here:\n\n```\nstruct str_split_out {\n    struct str_view *splits;\n    usize n_splits;\n    bool success;\n};\n// empty delimiter does not split, it returns one split\n// which contains the whole string\n[[nodiscard]]\nstruct str_split_out str_split(\n    struct arena *arena, struct str_view s,\n    struct str_view delim\n) {\n    struct str_split_out out = {0};\n    if (delim.len == 0) {\n        out.n_splits = 1;\n        struct arena_alloc_out alloc_out =\n            arena_alloc(arena, sizeof(struct str_view));\n        if (!alloc_out.success) {\n            return out;\n        }\n        out.splits = alloc_out.ptr;\n        *out.splits = s;\n        out.success = true;\n        return out;\n    }\n    out.n_splits = str_count_substr(s, delim) + 1;\n    struct arena_alloc_out alloc_out =\n        arena_alloc(arena, out.n_splits * sizeof(struct str_view));\n    if (!alloc_out.success) {\n        return out;\n    }\n    out.splits = alloc_out.ptr;\n    usize current_split_index = 0;\n    // last split index always points right after the previous delimiter,\n    // i.e. (splitting at \" \")\n    // \"abc abc\"\n    //      ^ last_split_index\n    //\n    // \" abc abc\"\n    //   ^ last_split_index\n    //\n    // or, before any split has been done:\n    // \"abc\"\n    //  ^ last_split_index\n    usize last_split_index = 0;\n    for (usize i = 0; i < s.len; i++) {\n        struct str_slice slice = {0};\n        slice.has_start = true;\n        slice.start = i;\n        struct str_view sliced_s = str_slice(s, slice);\n        if (!str_starts_with(sliced_s, delim))\n            continue;\n        i += delim.len;\n        struct str_view split = {0};\n        split.data = s.data + last_split_index;\n        split.len = i - last_split_index - delim.len;\n        out.splits[current_split_index] = split;\n        current_split_index++;\n        last_split_index = i;\n    }\n    struct str_view split = {0};\n    split.data = s.data + last_split_index;\n    split.len = s.len - last_split_index;\n    out.splits[out.n_splits - 1] = split;\n    out.success = true;\n    return out;\n}\n```\nAn empty delimiter does not split the string at all, it returns one split which contains\nthe entire string.\nThe delimiter can also be made of multiple characters (which is why I used a\n`struct str_view` instead of a `char`).\n\n### A Final Remark\n\nAnother thing to note is that `struct str_owned` are usually NUL-terminated. The `len`\nfield does not include the NUL-character, but it is there. This is a natural\nconsequence of creating a `struct str_owned` from a `struct str_view` created with the\n`STR` macro. Also, I try to keep this convention in my utility functions.\n\nThis makes it very nice to work with functions in the standard library that require\nyou to use NUL-terminated strings. Just pass the `data` field of the `struct str_owned`,\nand it acts as a normal C-string.\n\nAlso, some things are missing in this article that are actually implemented in my\nstring implementation that you can find in the repository. For example, one function\nthat is quite long I did not feel like pasting here was a `str_replace` function which\n\n- as the name suggests - replaces all occurrences of a substring in a string with something else.\n\n## Conclusions\n\nAll the code is available on\n[my codeberg repository](https://codeberg.org/eliasebner/c_libraries).\n\nI plan to add more utilities to this repository as time goes on, which is why the name has nothing to do with strings, but with libraries in general.\n\nI also wrote some tests for the string functions.\n\nThe idea is that you can simply copy the code that you need and put it in your own projects. Feel free to use it however you please.\n\nAlso, I have recently started to employ so-called “unity builds” for my projects.\nBasically, I do not use header files for my own code. Everything lives in `.c` files and I\n`#include` them directly.\n\nFor very large scale projects, this is not good practice, but for personal the scale of almost all personal projects this is completely fine and has the nice benefit of significantly speeding up compilation times, since it completely skips the linking step, which takes the most time.\n\nIf you do not like unity builds, you can create your own header file to hide the API of the string library.\n","body_html":"<p>I have always been frustrated about C not having a proper <code>String</code> type that most modern\nlanguages have nowadays. I love the simplicity of C, but there are some features that I\nwould really like to have in it, and this is one of them.</p>\n<p>Since I made a small library (just one <code>.c</code> file) that you can use to make the experience\nof working with strings in C just a little better, I would like to show it to you here.</p>\n<p>For the impatient, this is the general idea:</p>\n<pre><code>struct str_view {\n    const u8       *data;\n    usize           len;\n};\nstruct str_owned {\n    u8             *data;\n    usize           len;\n};</code></pre>\n<p>But before getting into the library itself, I will go over how “strings” work in C and why I find them annoying, what the issues are with this model, and then it will be easier to explain why I decided to take the route that I took.</p>\n<p>If you only care about the actual implementation details,\nyou can <a href=\"#the-solution\">click here</a>.</p>\n<h2 id=\"how-strings-work-in-c\">How Strings Work in C</h2>\n<p>As many might already know, strings in C are NUL-terminated. NUL is an ASCII character\nwhich, in the context of ASCII encoding, literally corresponds to the value 0.\nIn C, this character is represented by <code>&#39;\\0&#39;</code>. Inside a text, it generally does not\ncarry any semantic meaning, so it will never be used inside the text for the sake\nof carrying information (as opposed to letters like <code>&#39;a&#39;</code> or <code>&#39;y&#39;</code>).</p>\n<p>C does not really have “strings” per se. It only has arrays. These arrays are simply chunks of data where you can put things. To understand where the array ends, one has two options:</p>\n<ul><li>Store the length of the data somewhere</li><li>Put something at the end of the array to signify its end</li></ul>\n<p>C went with the second option.</p>\n<p>Since “strings” (which are not really strings) in C are simply arrays of characters, each string has a NUL-terminator at the end of it.</p>\n<p>C is a very old language and back then - when it was first created - this seemed like a good idea.</p>\n<p>Storing the length of the data separately required more memory than a NUL-character at the end. A NUL-character in ASCII encoding is 8 bits (or 1 byte). If you tried to store the length of the data in only 8 bits, you could only have strings that are up to 255 characters in length, which is obviously too short. Bigger numbers require more data to store, so we’re talking about around 4 to 8 bytes instead of 1.</p>\n<p>But this is not even the main reason - the main reason is because of simplicity. Back when C was created, there existed some languages that already had strings with a separately stored length. C just wanted to be as simple as possible, and having NUL-terminated character arrays seemed like a sane decision.</p>\n<h2 id=\"the-problems-with-this-approach\">The Problems With This Approach</h2>\n<p>There are multiple issues with this.</p>\n<h3 id=\"first-issue\">First Issue</h3>\n<p>First of all, if you want to find out the length of a string, with a NUL-terminated string\nyou have to start at index <code>0</code> and start counting until you reach the end of the data.\nThis process has a time complexity of , as opposed to  for just reading the\nseparately stored length.</p>\n<p>This is fine for short character arrays, but imagine you had to figure out the length of a character text. That would take a considerable amount of CPU cycles that could have been used for something else.</p>\n<p>This bothers me especially in a language like C, since it is supposed to give you some of the best performance that programming languages have to offer (without counting assembly and certain exceptions in some specific scenarios). C is often the baseline when comparing languages to gauge their relative performance. Why, if C focuses so much on performance, would you settle for this?</p>\n<h3 id=\"second-issue\">Second Issue</h3>\n<p>Let us assume that we have some C-string <code>some_str</code>:</p>\n<p><code>char *some_str = &quot;Hello World&quot;;</code>\nThis is great, but now I would like to work with the second word specifically (<code>World</code>).\nI do not care about changing the data, just about reading. Maybe I want to pass it to a\nfunction or something.\nWell, I could do something like this:</p>\n<p><code>char *only_world = some_str + 6;</code>\nBy doing this, we are reusing the NUL-terminator that <code>some_str</code> was using to terminate\nitself.</p>\n<p>Fine, what if I want to access the first word only, though? Now we have a problem.\nWe somehow have to end the string at the fifth index. To do that, we would have to\ninsert a NUL-character at index <code>6</code>. By doing so, we are changing the data that <code>some_str</code>\nis pointing to, and we can therefore not use that variable anymore.</p>\n<p>So, what do we do? We have to copy the string. That’s slow, since it involves memory lookups. We have to allocate to memory, and then copy it. We can also use the stack if we know the size of the data at compile time, but that is often not the case. That would look like this:</p>\n<pre><code>char *only_hello = malloc(6);\nmemcpy(only_hello, some_str, 5);\nonly_hello[5] = 0;</code></pre>\n<p>In this simple example, the stack would have been fine, but in most realistic scenarios, we have to use heap allocations.</p>\n<p>This problem is solved by so-called “string views”, which are basically immutable data and a\nlength. You can just give the string view the same pointer as <code>some_str</code> and decrease the\nlength to whatever you need, <code>5</code> in this case.</p>\n<h3 id=\"third-issue\">Third Issue</h3>\n<p>God forbid you forget to NUL-terminate a string. If you pass a pointer to a non-NUL-terminated\nstring to a function in <code>string.h</code>, like <code>strlen</code>, the computer will just keep counting\ncharacters until it either randomly finds a NUL-character or - alternatively - until it\ntries to access a region of memory that it should not touch and the operating system\nkills the process.</p>\n<p>This generally does not happen with actual strings (or string views), since you will use functions to interact with them, and they always keep the length of the string in sync with the data. In that case you always know when the string stops, you do not have to rely on remembering to NUL-terminate all your strings.</p>\n<h3 id=\"other-issues\">Other Issues</h3>\n<p>There are probably more issues, but these are hopefully enough to convince you. In general, I find strings with a separately stored length more intuitive and easier to work with.</p>\n<h2 id=\"the-solution\">The Solution</h2>\n<p>Luckily, strings are not particularly hard to implement in C. Here is how I did it.</p>\n<h3 id=\"types\">Types</h3>\n<p>Just so you are aware when reading the code, I <code>typedef</code> some basic types in C before I\nstart coding. In this case we will only need a few of them, but this is what I paste at\nthe start of every project of mine.</p>\n<pre><code>#include &lt;stdint.h&gt;\n#include &lt;stdbool.h&gt;\n#include &lt;stddef.h&gt;\ntypedef uint8_t     u8;\ntypedef uint16_t    u16;\ntypedef uint32_t    u32;\ntypedef uint64_t    u64;\ntypedef int8_t      i8;\ntypedef int16_t     i16;\ntypedef int32_t     i32;\ntypedef int64_t     i64;\ntypedef float       f32;\ntypedef double      f64;\ntypedef size_t      usize;</code></pre>\n<p>This makes it more convenient to use the integer types with a fixed size instead\nof the older <code>int</code>, <code>long</code>, <code>long long</code>, etc.</p>\n<p>I generally prefer this since <code>int</code> is not the same size on all platforms.\n<code>int32_t</code> on the other hand is guaranteed to be exactly 32 bits.</p>\n<h3 id=\"arena-allocator\">Arena Allocator</h3>\n<p>For your information, I have adapted this library to be compatible with my arena implementation.\nIt can be found in the same repository as this string library. I have an article about it\n<a href=\"/blog/guides/arena-allocators-in-c/\">here</a> if you would like to read up on that.</p>\n<p>What you need to know is that each function that needs to allocate some memory receives\na <code>struct arena *</code> as its first argument. Instead of <code>malloc</code> and <code>realloc</code> I use\n<code>arena_alloc</code> and <code>arena_realloc</code> respectively.</p>\n<h3 id=\"structs\">Structs</h3>\n<p>First, let’s define our string types. I don’t prefer not to <code>typedef</code> structs, but\nif you want to you can do so. If you do decide to <code>typedef</code> these structs,\nI would advise you to use <code>PascalCase</code> as a naming convention (to better differentiate them\nfrom built-in types).</p>\n<pre><code>struct str_view {\n    const u8       *data;\n    usize           len;\n};\nstruct str_owned {\n    u8             *data;\n    usize           len;\n};</code></pre>\n<p>As you can see, we have two string types: <code>struct str_view</code> and <code>struct str_owned</code>.\nThe difference is that <code>struct str_owned</code> owns the data, and the data can also be\nchanged. Ideally, you should try to only have one <code>str_owned</code> pointing at any given\nregion in memory.</p>\n<p>On the other hand, <code>struct str_view</code> should be used as read-only views of a\ngiven region of data. It should not be used to modify the data.</p>\n<p>Separating these two concerns is nice for multiple reasons. Who owns the data becomes clear immediately. This is especially useful when returning string structs from functions. By using this model, it is easier to understand whether we need to concern ourselves with freeing data or something similar.</p>\n<p>As a general rule of thumb, <code>struct str_owned</code> are heap-allocated.\nIf you have a <code>struct str_view</code> viewing some region in memory owned by\na <code>struct str_owned</code>, that string view is only valid as long as the underlying\nowned string is still valid. That means that you have to be careful about\nnot freeing the <code>struct str_owned</code> and after that still using string views\nthat view that data.</p>\n<p>Also, you might have noticed that I am using <code>u8</code>s (a.k.a. <code>uint8_t</code>s) instead of\n<code>char</code>s. That is because, technically - even though in almost all cases it does not matter -\n<code>char</code>s are not guaranteed to have a size of exactly 8 bits.\nOn certain embedded systems, a <code>char</code> is sometimes 16 or 32 bits large.\nOn very old systems, they used to be 9 bits. This has historical reasons.\nA <code>char</code> is simply guaranteed to have a size of one byte, but the exact size of a byte\nis not clearly defined as a definitive size in bits. Knowing all that, using <code>u8</code> is more\nconsistent across platforms.</p>\n<p>As a side note, this is also the reason why the <code>CHAR_BIT</code> macro exists. It tells you\nhow many bits are in a <code>char</code> on the system that the code is run. That is because the\nsize of a <code>char</code> depends on the system that you are currently on.</p>\n<p>Also, I do not really like using a redundant type. A <code>u8</code> holds all the semantic\ninformation I need, so <code>u8</code> it is.</p>\n<h3 id=\"macros\">Macros</h3>\n<p>Next, we create some macros. I try to minimize the use of macros, but in certain cases they are just perfect for the job. I believe that this is one of them.</p>\n<p>Mainly I care about the <code>STR</code> macro, that I use to easily create a <code>struct str_view</code>\nout of a <code>char *</code> literal.</p>\n<pre><code>#define LENGTHOF(s) (sizeof(s) / sizeof(*s))\n#define STR(s) (struct str_view){(const u8 *)s, LENGTHOF(s) - 1}\n#define STR_PRINT(s) (i32) s.len, s.data</code></pre>\n<p><code>STR_PRINT</code> is used to print a <code>struct str_view</code>. You can use the <code>%.*s</code> format specifier\nto manually give it the length of the string to be printed (since string views\nare not guaranteed to be NUL-terminated, since they can view a region of memory\nin the middle of a <code>struct str_owned</code>). This can be done as follows:</p>\n<p><code>printf(&quot;Here is my string: %.*s\\n&quot;, STR_PRINT(STR(&quot;some_string&quot;)));</code>\nHere you can also see the <code>STR</code> macro in action, which creates a <code>struct str_view</code> on the fly\nfrom a <code>char *</code> literal.</p>\n<h3 id=\"freeing-memory\">Freeing Memory</h3>\n<p>Since my implementation uses memory arenas, there is no need to free an owned string\nspecifically, but if you would like to move away from arenas you can always create\na simple wrapper function that calls <code>free</code> on the <code>data</code> field of an owned string.</p>\n<h3 id=\"conversion-functions\">Conversion Functions</h3>\n<p>We need some way to convert between <code>struct str_view</code> and <code>struct str_owned</code> and back.\nConverting an owned string to a string view is trivial; you simply use the same pointer\nand length of the owned string in a <code>struct str_view</code>:</p>\n<pre><code>[[nodiscard]]\nstruct str_view str_owned_to_str_view(struct str_owned s) {\n    struct str_view str_out = {0};\n    str_out.data = s.data;\n    str_out.len = s.len;\n    return str_out;\n}</code></pre>\n<p>Also, as you might have noticed, I like using <code>[[nodiscard]]</code>. This is a compiler\ndirective which makes the compiler issue a warning if the return value of the function\nflagged with this directive is disregarded.\nIn scenarios like this one I find it to be very useful, since it is completely useless\nto call this function if you are not going to do anything with the result.</p>\n<p>We also need to convert string views to owned strings. This is a bit more tricky and should not be done unless strictly necessary, since it requires memory allocation. An owned string can be mutated, which means that we have to give it its own region in memory to avoid damaging other owned strings that might be using the same memory (recall when I said that we should try to avoid having multiple owned strings on the same chunk of memory).</p>\n<pre><code>struct str_view_to_str_owned_out {\n    struct str_owned str;\n    bool success;\n};\n[[nodiscard]]\nstruct str_view_to_str_owned_out str_view_to_str_owned(\n    struct arena *arena,\n    struct str_view s\n) {\n    struct str_view_to_str_owned_out out = {0};\n    struct arena_alloc_out alloc_out = arena_alloc(arena, s.len + 1);\n    if (!alloc_out.success)\n        return out;\n    out.str.data = alloc_out.ptr;\n    memcpy(out.str.data, s.data, s.len);\n    out.str.len = s.len;\n    out.str.data[out.str.len] = 0;\n    out.success = true;\n    return out;\n}</code></pre>\n<p>As a small side note, whenever I need to return multiple values from a function I don’t like using output parameters (which is a technique often used in C programs). I prefer to return a struct. Also, it does not allow the caller of the function to use the return value directly, which makes it more likely that the programmer will remember to check for errors.</p>\n<p>It makes code very verbose but more consistent in my opinion, as opposed to output parameters. If you don’t like it, moving away from this system and adapting the code to your own needs should be trivial.</p>\n<h3 id=\"utility-functions\">Utility Functions</h3>\n<p>I also wrote a bunch of functions that implement common string operations and hopefully make the experience of working with strings much more ergonomic than the standard library.</p>\n<h4 id=\"checking-whether-a-string-starts-with-a-substring\">Checking Whether a String Starts With a Substring</h4>\n<pre><code>[[nodiscard]]\nbool str_starts_with(struct str_view s, struct str_view start) {\n    if (s.len &lt; start.len)\n        return false;\n    return 0 == memcmp(s.data, start.data, start.len);\n}</code></pre>\n<h4 id=\"slicing-strings\">Slicing Strings</h4>\n<p>This should behave similarly to the <code>String.slice</code> function in JavaScript.</p>\n<pre><code>struct str_slice {\n    i32 start;\n    bool has_start;\n    i32 end;\n    bool has_end;\n};\nstruct str_view str_slice(struct str_view s, struct str_slice slice) {\n    struct str_view out = {0};\n    if (slice.has_start) {\n        if (slice.start &lt; 0) {\n            slice.start = s.len + slice.start;\n        }\n    } else {\n        slice.start = 0;\n    }\n    if (slice.has_end) {\n        if (slice.end &lt; 0) {\n            slice.end = s.len + slice.end;\n        }\n    } else {\n        slice.end = s.len;\n    }\n    out.data = s.data + slice.start;\n    i32 out_len = slice.end - slice.start;\n    out.len = out_len &gt;= 0 ? out_len : 0;\n    return out;\n}</code></pre>\n<p>I decided to use a <code>struct</code> for the input parameter, because this makes it\neasier to support optional arguments. You can simply initialize it like this</p>\n<p><code>struct str_slice slice = {0};</code>\nAnd then, by default, both boolean fields are false. You can then set whatever fields\nyou want. If you do not provide a <code>start</code>, the default is <code>0</code>. If you do not\nprovide an <code>end</code>, the default is the length of the string view.</p>\n<p><code>start</code> is inclusive, while <code>end</code> is exclusive.\nI could not use negative values for representing “unset” values. For one, I really dislike\nthis practice, and also I wanted to give the user the option of passing negative\nvalues which reference the end of the string.</p>\n<p>What I mean is that if you pass <code>-1</code> as <code>start</code>, the function will start slicing at\nthe last character of the string. <code>-2</code> would be second to last.</p>\n<p>This is really ergonomic whenever you only want to shave off some characters at the end\n(for example, the <code>\\r\\n\\r\\n</code> at the end of the head of an HTTP string).</p>\n<h4 id=\"finding-a-substring-in-a-string\">Finding a Substring in a String</h4>\n<p>I loop keep slicing away one character at a time from a string and check if the resulting string starts with the desired substring.</p>\n<pre><code>struct str_find_substr_out {\n    usize idx;\n    bool found;\n};\n[[nodiscard]]\nstruct str_find_substr_out str_find_substr(\n    struct str_view s,\n    struct str_view sub_s\n) {\n    struct str_find_substr_out out = {0};\n    if (s.len &lt; sub_s.len)\n        return out;\n    usize match_index = 0;\n    for (usize i = 0; i &lt; s.len; i++) {\n        struct str_slice slice = {0};\n        slice.has_start = true;\n        slice.start = i;\n        struct str_view s_slice = str_slice(s, slice);\n        if (str_starts_with(s_slice, sub_s)) {\n            out.found = true;\n            out.idx = i;\n            return out;\n        }\n    }\n    return out;\n}</code></pre>\n<h4 id=\"determining-whether-a-string-contains-a-substring\">Determining Whether a String Contains a Substring</h4>\n<p>For this, I simply use the <code>str_find_substr</code> function defined above and extract the <code>found</code>\nfield from the <code>struct str_find_out</code> output.</p>\n<pre><code>[[nodiscard]]\nbool str_contains_substr(struct str_view s, struct str_view sub_s) {\n    return str_find_substr(s, sub_s).found;\n}</code></pre>\n<h4 id=\"determining-whether-two-strings-are-equal\">Determining Whether Two Strings Are Equal</h4>\n<p>This is straight forward. Check the lengths and compare the memory.</p>\n<pre><code>[[nodiscard]]\nbool str_eq(struct str_view s1, struct str_view s2) {\n    if (s1.len != s2.len)\n        return false;\n    return 0 == memcmp(s1.data, s2.data, s1.len);\n}</code></pre>\n<p>Here is the same thing, but it ignores the casing. Adding <code>32</code> to an uppercase letter\ncharacter converts it to the lowercase equivalent. This, of course, only works\nwith ASCII encodings.</p>\n<pre><code>[[nodiscard]]\nbool str_eq_ignore_case(struct str_view s1, struct str_view s2) {\n    if (s1.len != s2.len)\n        return false;\n    for (usize i = 0; i &lt; s1.len; i++) {\n        u8 c1 = s1.data[i];\n        u8 c2 = s2.data[i];\n        if (c1 &gt;= 65 &amp;&amp; c1 &lt;= 90)\n            c1 += 32;\n        if (c2 &gt;= 65 &amp;&amp; c2 &lt;= 90)\n            c2 += 32;\n        if (c1 != c2)\n            return false;\n    }\n    return true;\n}</code></pre>\n<h4 id=\"splitting-strings\">Splitting Strings</h4>\n<p>This one is a bit more complicated, since it involves heap allocations. At first, I implemented it in a way that required a memory allocation for each new split that was found. I later changed it and created this helper function:</p>\n<pre><code>u32 str_count_substr(struct str_view s, struct str_view sub_s) {\n    u32 out = 0;\n    usize sub_s_index = 0;\n    for (usize i = 0; i &lt; s.len; i++) {\n        struct str_slice slice = {0};\n        slice.has_start = true;\n        slice.start = i;\n        struct str_view sliced_s = str_slice(s, slice);\n        if (str_starts_with(sliced_s, sub_s))\n            out++;\n    }\n    return out;\n}</code></pre>\n<p>This counts the number of occurrences of a substring before splitting, which is useful for allocating all the required memory upfront. The number of splits will be the number of occurrences of the delimiter plus one.</p>\n<p>The actual splitting happens here:</p>\n<pre><code>struct str_split_out {\n    struct str_view *splits;\n    usize n_splits;\n    bool success;\n};\n// empty delimiter does not split, it returns one split\n// which contains the whole string\n[[nodiscard]]\nstruct str_split_out str_split(\n    struct arena *arena, struct str_view s,\n    struct str_view delim\n) {\n    struct str_split_out out = {0};\n    if (delim.len == 0) {\n        out.n_splits = 1;\n        struct arena_alloc_out alloc_out =\n            arena_alloc(arena, sizeof(struct str_view));\n        if (!alloc_out.success) {\n            return out;\n        }\n        out.splits = alloc_out.ptr;\n        *out.splits = s;\n        out.success = true;\n        return out;\n    }\n    out.n_splits = str_count_substr(s, delim) + 1;\n    struct arena_alloc_out alloc_out =\n        arena_alloc(arena, out.n_splits * sizeof(struct str_view));\n    if (!alloc_out.success) {\n        return out;\n    }\n    out.splits = alloc_out.ptr;\n    usize current_split_index = 0;\n    // last split index always points right after the previous delimiter,\n    // i.e. (splitting at &quot; &quot;)\n    // &quot;abc abc&quot;\n    //      ^ last_split_index\n    //\n    // &quot; abc abc&quot;\n    //   ^ last_split_index\n    //\n    // or, before any split has been done:\n    // &quot;abc&quot;\n    //  ^ last_split_index\n    usize last_split_index = 0;\n    for (usize i = 0; i &lt; s.len; i++) {\n        struct str_slice slice = {0};\n        slice.has_start = true;\n        slice.start = i;\n        struct str_view sliced_s = str_slice(s, slice);\n        if (!str_starts_with(sliced_s, delim))\n            continue;\n        i += delim.len;\n        struct str_view split = {0};\n        split.data = s.data + last_split_index;\n        split.len = i - last_split_index - delim.len;\n        out.splits[current_split_index] = split;\n        current_split_index++;\n        last_split_index = i;\n    }\n    struct str_view split = {0};\n    split.data = s.data + last_split_index;\n    split.len = s.len - last_split_index;\n    out.splits[out.n_splits - 1] = split;\n    out.success = true;\n    return out;\n}</code></pre>\n<p>An empty delimiter does not split the string at all, it returns one split which contains\nthe entire string.\nThe delimiter can also be made of multiple characters (which is why I used a\n<code>struct str_view</code> instead of a <code>char</code>).</p>\n<h3 id=\"a-final-remark\">A Final Remark</h3>\n<p>Another thing to note is that <code>struct str_owned</code> are usually NUL-terminated. The <code>len</code>\nfield does not include the NUL-character, but it is there. This is a natural\nconsequence of creating a <code>struct str_owned</code> from a <code>struct str_view</code> created with the\n<code>STR</code> macro. Also, I try to keep this convention in my utility functions.</p>\n<p>This makes it very nice to work with functions in the standard library that require\nyou to use NUL-terminated strings. Just pass the <code>data</code> field of the <code>struct str_owned</code>,\nand it acts as a normal C-string.</p>\n<p>Also, some things are missing in this article that are actually implemented in my\nstring implementation that you can find in the repository. For example, one function\nthat is quite long I did not feel like pasting here was a <code>str_replace</code> function which</p>\n<ul><li>as the name suggests - replaces all occurrences of a substring in a string with something else.</li></ul>\n<h2 id=\"conclusions\">Conclusions</h2>\n<p>All the code is available on\n<a href=\"https://codeberg.org/eliasebner/c_libraries\" rel=\"nofollow ugc noopener\">my codeberg repository</a>.</p>\n<p>I plan to add more utilities to this repository as time goes on, which is why the name has nothing to do with strings, but with libraries in general.</p>\n<p>I also wrote some tests for the string functions.</p>\n<p>The idea is that you can simply copy the code that you need and put it in your own projects. Feel free to use it however you please.</p>\n<p>Also, I have recently started to employ so-called “unity builds” for my projects.\nBasically, I do not use header files for my own code. Everything lives in <code>.c</code> files and I\n<code>#include</code> them directly.</p>\n<p>For very large scale projects, this is not good practice, but for personal the scale of almost all personal projects this is completely fine and has the nice benefit of significantly speeding up compilation times, since it completely skips the linking step, which takes the most time.</p>\n<p>If you do not like unity builds, you can create your own header file to hide the API of the string library.</p>","headings":[{"level":2,"text":"How Strings Work in C","id":"how-strings-work-in-c"},{"level":2,"text":"The Problems With This Approach","id":"the-problems-with-this-approach"},{"level":3,"text":"First Issue","id":"first-issue"},{"level":3,"text":"Second Issue","id":"second-issue"},{"level":3,"text":"Third Issue","id":"third-issue"},{"level":3,"text":"Other Issues","id":"other-issues"},{"level":2,"text":"The Solution","id":"the-solution"},{"level":3,"text":"Types","id":"types"},{"level":3,"text":"Arena Allocator","id":"arena-allocator"},{"level":3,"text":"Structs","id":"structs"},{"level":3,"text":"Macros","id":"macros"},{"level":3,"text":"Freeing Memory","id":"freeing-memory"},{"level":3,"text":"Conversion Functions","id":"conversion-functions"},{"level":3,"text":"Utility Functions","id":"utility-functions"},{"level":3,"text":"A Final Remark","id":"a-final-remark"},{"level":2,"text":"Conclusions","id":"conclusions"}]}}