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* pas this is marked as UB.
- Use C library like sdsif 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";orxstrdup("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')... | callocormemsetbefore read | 
| Realloc UB | p=realloc(p,new); use(p+old..) | Always memsetthe 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 sdsif you don't want to handle string manually.
- NOT use literal on char* pto avoid confusion duringfree().
- 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 pointerNULL.
- Never evaluate uninitialized memory.
✅ Summary
- 
As per most C API Standard, DO NOT use literal on char* pto avoid confusion duringfree().
- 
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.
 
 
              
 
    
Top comments (0)