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 | ![]() |
![]() |
![]() |
char buf[16] = "abc"; |
array | ![]() |
![]() |
![]() |
char *p = xstrdup("abc"); |
heap | ![]() |
![]() |
![]() |
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 | ![]() |
Pointer → literal/borrowed | reassign pointer | ![]() |
Pointer → heap (owned) | overwrite (fits cap) | ![]() |
Pointer → heap (owned) | replace (new alloc) | ![]() |
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 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* p
to 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.
This content originally appeared on DEV Community and was authored by L Djohari