The Guide into Strings in C for Dev who wanted to learns C – Clear Rules, Mutation, Reassignment & Ownership, No Surprises



This content originally appeared on DEV Community and was authored by L Djohari

C++ has std::string with RAII, move semantics, and safe copying.
C has only pointers and arrays. You are responsible for allocation, mutation, reassignment, and freeing. You own lifetime and memory.
That’s where most bugs and shot in the foot happen in C.

This guide is for C++ or migrated from any languages who want safe, modern C string rules without ambiguity and no surprises.

1. The Three Faces of Strings

Form Storage Can change contents? Can reassign pointer? Need free?
const char *p = "abc"; literal ❌ (read-only) ✅ yes ❌ no
char buf[16] = "abc"; array ✅ (in place) ❌ (arrays not assignable) ❌ no
char *p = xstrdup("abc"); heap ✅ (if capacity fits) ✅ yes ✅ yes

➡ Rules of thumb:

  • Literal: borrowed, immutable, lives forever.
  • Array: embedded, mutable, no free.
  • Heap pointer: owned, mutable, must free.
  • char *p: In most C API DO NOT ASSIGN STRING LITERAL if you found field char* p as this is marked as UB.
  • Use C library like sds if you don’t want to handle string manually.

2. Null Terminators

  • Literals: always null-terminated.
  • Arrays: must have room for \0 .
  • Heap strings: always allocate +1 for terminator.

✅ Always use snprintf for bounded, safe writes:

char buf[16];
snprintf(buf, sizeof buf, "%s", "127.0.0.1");  // auto NUL

3. Do You Need to free Before Changing?

Case Action Free first?
Array char buf[N] overwrite in place ❌ never
Pointer → literal/borrowed reassign pointer ❌ never
Pointer → heap (owned) overwrite (fits cap) ❌ no
Pointer → heap (owned) replace (new alloc) ✅ yes

➡ If unsure about capacity → safest is always:

free(p);
p = xstrdup(new_value);

4. Helper functions

xstrdup (unbounded-length)

Note: xstrdup(str) is my custom function to handle copy-on-set when size is unknown. You must ensured if string is valid c-string with \0 terminator, otherwise it is an UB.

#include <stdlib.h>
#include <string.h>
#include <errno.h>

/* C-string duplicate:
   - s == NULL -> returns owned "".
   - MUST BE VALID c-string with \0 terminated (caller’s contract).
*/
static inline char *xstrdup(const char *s) {
    if (!s) { 
        char *z = malloc(1); 
        if (!z){
            return NULL; 
        } 
        z[0] = '\0'; return z; 
    }

    size_t n = strlen(s);
    char *p = malloc(n + 1);

    if (!p){ 
        return NULL;
    }

    memcpy(p, s, n + 1);
    return p;
}

xstrndup (bounded-length)

#include <stdlib.h>
#include <string.h>
#include <limits.h>

/* Policy: NULL or maxlen==0 -> return an owned empty string */
static inline char *xstrndup(const char *s, size_t maxlen) {

    if (s == NULL || maxlen == 0) {
        char *z = malloc(1);
        if (!z) {
            return NULL;
        }
        z[0] = '\0';
        return z;
    }

    /* Guard (maxlen + 1) overflow */
    if (maxlen == SIZE_MAX) {
        return NULL;
    }

    size_t n = strnlen(s, maxlen);

    char *p = malloc(n + 1);
    if (!p) {
        return NULL;
    }

    if (n) {
        memcpy(p, s, n);
    }

    p[n] = '\0';
    return p;
}

5. Practical Cases

A) Pointer to literal (DO NOT USE)

This pattern you must avoid. As per most C API Contract are not allowed this pattern because it will introduce confusion when freeing the memory.

char *p = "0.0.0.0";    // borrowed literal
p = "127.0.0.1";        // ✅ reassign to another literal
// p[0] = '1';          // ❌ UB: can't modify literal
// free(p);             // ❌ UB: don't free literals

B) Literal first, then reassigned to owned copy

This pattern must be to avoid declaring char *p with literal string at first place, but it is safe.

char *p = "0.0.0.0";    // starts as borrowed literal
p = xstrdup(p); // ✅ now heap-owned, must free later
if(p){
    free(p); // ✅ now heap-owned, must free late
    p = NULL;
}

Key point: after reassignment, ownership changes → now you mus t manage lifetime.

C) Array inside struct

struct S { char host[16]; } s = {0};
snprintf(s.host, sizeof(s.host), "%s", "0.0.0.0");
snprintf(s.host, sizeof(s.host), "%s", "127.0.0.1");  // ✅ overwrite in place
// never free, buffer belongs to struct

D) Owned pointer inside struct (copy-on-set)

struct S { char *host; } s = {0};
s.host = xstrdup("0.0.0.0");

char *tmp = xstrdup("127.0.0.1");
free(s.host);
s.host = tmp;

free(s.host);
s.host = NULL;

E) Owned pointer (mutate in place with capacity)

char *p = malloc(64);
snprintf(p, 64, "%s", "0.0.0.0");
snprintf(p, 64, "%s", "127.0.0.1");  // ✅ fits
free(p);
p = NULL;

F) Start as literal, then copy, then replace

char *p = "init";          // literal, borrowed
p = xstrdup(p);            // now owned heap copy
replace_owned_string(&p, "newvalue"); // frees + assigns safely
free(p);
p = NULL;

6. Expanded Bug Cases to Avoid

Bug Bad Code Fix
Modify literal char *p="abc"; p[0]='A'; Use char buf[]="abc"; or xstrdup("abc")
Free literal char *p="abc"; free(p); Never free literals
Double free free(p); free(p);
Use-after-free free(p); p = NULL;  printf("%s", p); Always set to NULL, check before use
Buffer overflow strcpy(buf,"longstring"); Use snprintf(buf,sizeof buf,"%s",src)
Uninit read char *p=malloc(16); if(p[0]=='a')... calloc or memset before read
Realloc UB p=realloc(p,new); use(p+old..) Always memset the new region

7. API Pattern: Copy-on-Set

void replace_owned_string(char **dst, const char *src) {
    char *copy = xstrdup(src);
    free(*dst);
    *dst = copy;
}

Usage:

struct S { char *host; } s = {0};
replace_owned_string(&s.host, "0.0.0.0");
replace_owned_string(&s.host, "127.0.0.1");
free(s.host);
s.host = NULL;

8. Choosing Array vs Pointer

  • Array (char field[N]): fixed-size protocol fields, ISO8583 Data Elements, IPv4, timestamps.
  • Pointer (char *field): unbounded input, user-provided data.

➡ Arrays = simpler, no free.
➡ Pointers = flexible, but you must free or replace carefully.

9. Quick Checklist

  • Use C library like sds if you don’t want to handle string manually.
  • NOT use literal on char* p to avoid confusion during free().
  • Literals: reassign pointer only; no mutate/free.
  • Arrays: mutate in place with snprintf; no free.
  • Heap-owned: mutate if fits; else free old + copy new.
  • If unsure always replace_owned_string.
  • After free() set pointer NULL.
  • Never evaluate uninitialized memory.

✅ Summary

  • As per most C API Standard, DO NOT use literal on char* p to avoid confusion during free().
  • char *p = "value"; → pointer to literal (borrowed).
  • Reassigning to another literal = fine.
  • Reassigning to xstrdup("...") = now owned → must free.
  • Arrays are safe, fixed buffers.
  • Heap pointers need manual lifetime control.
  • Copy-on-set pattern keeps your code boring and safe.


This content originally appeared on DEV Community and was authored by L Djohari