DEV Community

Cover image for The Guide into Strings in C for Dev who wanted to learns C - Clear Rules, Mutation, Reassignment & Ownership, No Surprises
L Djohari
L Djohari

Posted on

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

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

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

Top comments (0)