|$ curl https://forge-ai.dev/api/markdown?path=docs/c/strings
$cat docs/c-—-strings.md
updated Recently·40 min read·published

C — Strings

CStringsCharacter ArraysIntermediateIntermediate🎯Free Tools
What Is a String in C?

C has no native string type. A string is simply a null-terminated array of characters — the character `\0` (ASCII value 0) marks the end. This design gives you full control over memory and encoding, but it also means every string operation must respect the null terminator or risk undefined behavior. Understanding how strings work at the byte level is essential for writing safe, correct C code.

String Declaration

There are two primary ways to declare a string in C. They look similar but have critical differences in mutability, storage, and lifetime.

string_declaration.c
C
1// Char array (mutable, stored on the stack)
2char greeting[] = "Hello";
3// Compiler allocates 6 bytes: H e l l o \0
4greeting[0] = 'h'; // VALID — it's a mutable array
5
6// Pointer to string literal (immutable, stored in read-only memory)
7const char *msg = "Hello";
8msg[0] = 'h'; // UNDEFINED BEHAVIOR — string literals are read-only
9
10// Array of pointers to string literals
11const char *colors[] = {"Red", "Green", "Blue"};
12
13// Pointer to dynamically allocated string
14char *dynamic = malloc(6);
15strcpy(dynamic, "Hello");
16dynamic[0] = 'h'; // VALID — it's heap memory

warning

String literals like "hello" are stored in read-only memory. Assigning them to a char * (without const) compiles but may crash when you try to modify them. Always use const char * for string literals, or use char arrays for mutable strings.
The Null Terminator

The null terminator `\0` is what makes a char array a string. Without it, functions like printf, strlen, and strcpy will read past the array into adjacent memory. Every string operation depends on finding that null byte.

string_null.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(void) {
5 // These two are equivalent — compiler adds \0 automatically
6 char a[] = "Hi"; // {'H', 'i', '\0'} — 3 bytes
7 char b[] = {'H', 'i', '\0'}; // same thing
8
9 // This is NOT a string — no null terminator!
10 char c[] = {'H', 'i'}; // {'H', 'i'} — 2 bytes, no \0
11
12 printf("%s\n", a); // "Hi" — works
13 printf("%s\n", b); // "Hi" — works
14 // printf("%s\n", c); // UNDEFINED — reads past array looking for \0
15
16 // strlen counts characters BEFORE the null terminator
17 printf("strlen(a) = %zu\n", strlen(a)); // 2
18 printf("sizeof(a) = %zu\n", sizeof(a)); // 3 (includes \0)
19
20 return 0;
21}
String I/O

Reading and writing strings is where most security vulnerabilities originate in C. The standard I/O functions range from extremely dangerous to reasonably safe.

string_io.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(void) {
5 char name[50];
6
7 // printf with %s — prints until null terminator
8 printf("Hello, %s!\n", "World");
9
10 // scanf with %s — STOPS at whitespace, no bounds checking
11 // DANGEROUS: if user types more than 49 chars, buffer overflow!
12 printf("Enter your name: ");
13 scanf("%49s", name); // width specifier limits input
14
15 // fgets — safe and recommended
16 // Reads up to n-1 characters, always null-terminates
17 printf("Enter a line: ");
18 fgets(name, sizeof(name), stdin);
19
20 // fgets keeps the newline — remove it
21 name[strcspn(name, "\n")] = '\0';
22
23 printf("You entered: %s\n", name);
24
25 return 0;
26}

info

Never use gets(). It was removed from the C11 standard because it has no way to prevent buffer overflow. Always use fgets() with a size limit, and always strip the trailing newline.
String Length

The strlen() function returns the number of characters before the null terminator. It does NOT count the null terminator. This is different from sizeof, which gives the total allocated size.

string_length.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(void) {
5 char str[] = "Hello";
6
7 printf("strlen(str) = %zu\n", strlen(str)); // 5
8 printf("sizeof(str) = %zu\n", sizeof(str)); // 6 (includes \0)
9
10 // strlen walks memory until it finds \0 — O(n) operation
11 // You can write your own to understand the mechanism
12 size_t my_strlen(const char *s) {
13 size_t len = 0;
14 while (s[len] != '\0') {
15 len++;
16 }
17 return len;
18 }
19
20 printf("my_strlen = %zu\n", my_strlen("Test")); // 4
21
22 return 0;
23}
String Copy

You cannot assign one string to another with =. Assignment only copies the pointer, not the content. You must use strcpy() or strncpy() to copy string data.

string_copy.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(void) {
5 char src[] = "Hello, World!";
6 char dst[50];
7
8 // strcpy — copies until \0, no bounds checking
9 strcpy(dst, src);
10 printf("dst: %s\n", dst); // "Hello, World!"
11
12 // strncpy — copies at most n characters
13 // WARNING: does NOT null-terminate if src is longer than n!
14 char partial[6];
15 strncpy(partial, src, sizeof(partial) - 1);
16 partial[sizeof(partial) - 1] = '\0'; // always null-terminate manually
17 printf("partial: %s\n", partial); // "Hello"
18
19 // Safe wrapper pattern
20 void safe_copy(char *dst, size_t dst_size, const char *src) {
21 if (dst_size == 0) return;
22 strncpy(dst, src, dst_size - 1);
23 dst[dst_size - 1] = '\0';
24 }
25
26 char buf[20];
27 safe_copy(buf, sizeof(buf), "This is a long string");
28 printf("buf: %s\n", buf); // "This is a long st"
29
30 return 0;
31}

warning

strncpy() does NOT guarantee null termination. If the source string is longer than the destination, the null terminator is omitted. Always add it manually after the call.
String Concatenation

strcat() appends one string to another. Like strcpy, it has no bounds checking. strncat() is safer because it limits the number of characters appended.

string_concat.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(void) {
5 char result[50] = "Hello";
6
7 // strcat — appends src to dst (no bounds check)
8 strcat(result, ", ");
9 strcat(result, "World!");
10 printf("%s\n", result); // "Hello, World!"
11
12 // strncat — append at most n characters
13 char prefix[20] = "Hello";
14 strncat(prefix, ", World! This is too long", 5);
15 printf("%s\n", prefix); // "Hello, Worl"
16
17 // Safe concatenation helper
18 size_t safe_cat(char *dst, size_t dst_size, const char *src) {
19 size_t dst_len = strlen(dst);
20 size_t src_len = strlen(src);
21 size_t copy_len = (dst_len + src_len < dst_size)
22 ? src_len
23 : dst_size - dst_len - 1;
24 memcpy(dst + dst_len, src, copy_len);
25 dst[dst_len + copy_len] = '\0';
26 return dst_len + copy_len;
27 }
28
29 char buf[30] = "Hello";
30 safe_cat(buf, sizeof(buf), ", World!");
31 printf("%s\n", buf); // "Hello, World!"
32
33 return 0;
34}
String Comparison

You cannot compare strings with ==. That compares pointer addresses, not content. Use strcmp() for lexicographic comparison and strncmp() for partial comparison.

string_compare.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(void) {
5 const char *a = "Apple";
6 const char *b = "Banana";
7 const char *c = "Apple";
8
9 // strcmp returns: 0 if equal, {'<'}0 if a{'<'}b, {'>'}0 if a{'>'}b
10 printf("strcmp(a, b) = %d\n", strcmp(a, b)); // negative (A {'<'} B)
11 printf("strcmp(a, c) = %d\n", strcmp(a, c)); // 0 (equal)
12 printf("strcmp(b, a) = %d\n", strcmp(b, a)); // positive (B {'>'} A)
13
14 // strncmp — compare at most n characters
15 printf("strncmp(a, c, 3) = %d\n", strncmp(a, c, 3)); // 0
16
17 // Common pattern: check for equality
18 if (strcmp(a, c) == 0) {
19 printf("Strings are equal\n");
20 }
21
22 // Case-insensitive comparison (not in standard C)
23 // tolower() each character manually or use strcasecmp() (POSIX)
24 printf("strcasecmp = %d\n", strcasecmp("hello", "HELLO")); // 0
25
26 // NEVER do this — compares pointers, not content
27 if (a == c) {
28 printf("This may or may not print depending on compiler\n");
29 }
30
31 return 0;
32}
String Conversion

Converting between strings and numbers is extremely common. C provides functions for this, but they all have important caveats.

string_conversion.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5int main(void) {
6 // String to integer
7 int a = atoi("42"); // 42
8 int b = atoi("abc"); // 0 (no error indication!)
9 long c = strtol("123abc", NULL, 10); // 123, stops at 'a'
10 long d = strtol("xyz", NULL, 10); // 0, errno = ERANGE
11
12 // Check for conversion errors properly
13 char *endptr;
14 long val = strtol("123abc", &endptr, 10);
15 if (*endptr != '\0') {
16 printf("Partial conversion: %ld, stopped at '%s'\n", val, endptr);
17 }
18
19 // String to float
20 double pi = atof("3.14159");
21 char *fend;
22 double precise = strtod("3.14abc", &fend);
23 printf("Converted: %f, remainder: %s\n", precise, fend);
24
25 // Integer to string
26 char buf[20];
27 sprintf(buf, "%d", 42); // "42" — no bounds checking!
28 snprintf(buf, sizeof(buf), "Value: %d", 42); // safe
29
30 // Format a float
31 char fbuf[20];
32 snprintf(fbuf, sizeof(fbuf), "%.2f", 3.14159); // "3.14"
33
34 printf("buf: %s, fbuf: %s\n", buf, fbuf);
35
36 return 0;
37}

best practice

Prefer strtol() over atoi() because strtol provides error detection through errno and the end pointer. Never use sprintf() without bounds — always use snprintf().
String Tokenization with strtok

strtok() splits a string into tokens based on delimiter characters. It modifies the original string by inserting null terminators. It is NOT thread-safe because it uses internal static state.

string_token.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(void) {
5 char csv[] = "apple,banana,cherry,date";
6
7 // First call with the string, subsequent calls with NULL
8 char *token = strtok(csv, ",");
9 while (token != NULL) {
10 printf("Token: %s\n", token);
11 token = strtok(NULL, ",");
12 }
13
14 // Tokenizing with multiple delimiters
15 char line[] = "Hello World\t\tC\nProgramming";
16 char *word = strtok(line, " \t\n");
17 while (word != NULL) {
18 printf("Word: %s\n", word);
19 word = strtok(NULL, " \t\n");
20 }
21
22 // Safer alternative: strtok_r (POSIX) — thread-safe
23 char data[] = "one:two:three";
24 char *saveptr;
25 char *token2 = strtok_r(data, ":", &saveptr);
26 while (token2 != NULL) {
27 printf("Token: %s\n", token2);
28 token2 = strtok_r(NULL, ":", &saveptr);
29 }
30
31 return 0;
32}
Memory-Safe Alternatives (BSD/GNU)

BSD and GNU systems provide safer versions of the standard string functions. These are widely available but not part of the C standard. They guarantee null termination and prevent buffer overflows.

string_safe.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(void) {
5 // strlcpy — guaranteed null-terminated copy
6 // Returns total length of src (like strlen)
7 char dst[10];
8 size_t len = strlcpy(dst, "Hello, World!", sizeof(dst));
9 printf("dst: %s (len=%zu, src_len=%zu)\n", dst, strlen(dst), len);
10 // dst: "Hello, Wor" (truncated), len=13
11
12 // strlcat — guaranteed null-terminated concatenation
13 char buf[20] = "Hello";
14 strlcat(buf, ", World!", sizeof(buf));
15 printf("buf: %s\n", buf); // "Hello, World!"
16
17 // If it would overflow, result is truncated but null-terminated
18 char small[8];
19 strlcpy(small, "Hello, World!", sizeof(small));
20 strlcat(small, " Bye", sizeof(small));
21 printf("small: %s\n", small); // "Hello, " — truncated safely
22
23 return 0;
24}
String Arrays (Array of Strings)

There are two common ways to store multiple strings: a 2D char array (fixed length per string) or an array of char pointers (variable length). Each has trade-offs.

string_array.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(void) {
5 // 2D char array — all strings have the same max length
6 // Wastes memory if strings vary widely in length
7 char names[][20] = {
8 "Alice",
9 "Bob",
10 "Charlie"
11 };
12 int name_count = sizeof(names) / sizeof(names[0]);
13
14 // Sort with qsort
15 int cmp_names(const void *a, const void *b) {
16 return strcmp((const char *)a, (const char *)b);
17 }
18 qsort(names, name_count, sizeof(names[0]), cmp_names);
19
20 for (int i = 0; i < name_count; i++) {
21 printf("Name %d: %s\n", i, names[i]);
22 }
23
24 // Array of pointers — variable length strings
25 // Each string lives in a different memory location
26 const char *fruits[] = {
27 "Apple",
28 "Banana",
29 "Cherry",
30 "Date",
31 "Elderberry"
32 };
33 int fruit_count = sizeof(fruits) / sizeof(fruits[0]);
34
35 for (int i = 0; i < fruit_count; i++) {
36 printf("Fruit: %s (len=%zu)\n", fruits[i], strlen(fruits[i]));
37 }
38
39 return 0;
40}
String Literals vs Mutable Strings

Understanding the difference between string literals and mutable character arrays is critical. String literals are stored in read-only memory. Modifying them is undefined behavior. Character arrays are stack-allocated and fully mutable.

string_literals.c
C
1#include <stdio.h>
2#include <string.h>
3#include <stdlib.h>
4
5int main(void) {
6 // String literal — stored in .rodata (read-only data segment)
7 const char *literal = "Hello";
8 // Modifying literal[0] = 'h' would be UB
9
10 // Mutable copy on the stack
11 char mutable[] = "Hello";
12 mutable[0] = 'h'; // Valid: "hello"
13
14 // Mutable copy on the heap
15 char *heap = malloc(6);
16 strcpy(heap, "Hello");
17 heap[0] = 'H'; // Valid
18 free(heap);
19
20 // String literal passed to function — const-correct
21 void print_str(const char *s) {
22 printf("%s\n", s);
23 }
24 print_str("Test");
25
26 // sizeof difference
27 printf("sizeof(literal): %zu\n", sizeof(literal)); // 8 (pointer size)
28 printf("sizeof(mutable): %zu\n", sizeof(mutable)); // 6 (array size)
29
30 return 0;
31}
Real-World String Algorithms

Here are practical string manipulation functions you'll use frequently. Each demonstrates safe memory handling and idiomatic C patterns.

Reverse a String In-Place

reverse_string.c
C
1void reverse_string(char *str) {
2 int len = strlen(str);
3 for (int i = 0; i < len / 2; i++) {
4 char tmp = str[i];
5 str[i] = str[len - 1 - i];
6 str[len - 1 - i] = tmp;
7 }
8}
9
10// Usage
11char s[] = "Hello";
12reverse_string(s);
13printf("%s\n", s); // "olleH"

Check Palindrome

palindrome.c
C
1int is_palindrome(const char *str) {
2 int left = 0;
3 int right = strlen(str) - 1;
4 while (left < right) {
5 if (str[left] != str[right]) {
6 return 0; // not a palindrome
7 }
8 left++;
9 right--;
10 }
11 return 1; // is a palindrome
12}
13
14// Usage
15printf("%d\n", is_palindrome("racecar")); // 1
16printf("%d\n", is_palindrome("hello")); // 0

Count Words in a String

count_words.c
C
1int count_words(const char *str) {
2 int count = 0;
3 int in_word = 0;
4
5 while (*str) {
6 if (*str == ' ' || *str == '\t' || *str == '\n') {
7 in_word = 0;
8 } else if (!in_word) {
9 in_word = 1;
10 count++;
11 }
12 str++;
13 }
14 return count;
15}
16
17// Usage
18printf("Words: %d\n", count_words(" Hello World ")); // 2
19printf("Words: %d\n", count_words("Single")); // 1

Trim Whitespace

trim_string.c
C
1void trim(char *str) {
2 // Trim leading whitespace
3 char *start = str;
4 while (*start == ' ' || *start == '\t' || *start == '\n') {
5 start++;
6 }
7
8 // Shift the string left
9 if (start != str) {
10 memmove(str, start, strlen(start) + 1);
11 }
12
13 // Trim trailing whitespace
14 char *end = str + strlen(str) - 1;
15 while (end >= str && (*end == ' ' || *end == '\t' || *end == '\n')) {
16 *end = '\0';
17 end--;
18 }
19}
20
21// Usage
22char text[] = " Hello, World! ";
23trim(text);
24printf("[%s]\n", text); // [Hello, World!]

Split String by Delimiter

split_string.c
C
1#include <stdio.h>
2#include <string.h>
3#include <stdlib.h>
4
5int split_string(const char *str, char delimiter,
6 char tokens[][64], int max_tokens) {
7 int count = 0;
8 const char *start = str;
9 const char *end;
10
11 while (count {'<'} max_tokens && *start) {
12 end = strchr(start, delimiter);
13 if (!end) end = start + strlen(start);
14
15 int len = end - start;
16 if (len >= 64) len = 63;
17 memcpy(tokens[count], start, len);
18 tokens[count][len] = '\0';
19 count++;
20
21 start = (*end) ? end + 1 : end;
22 }
23 return count;
24}
25
26// Usage
27char tokens[10][64];
28int n = split_string("apple,banana,cherry", ',', tokens, 10);
29for (int i = 0; i < n; i++) {
30 printf("Token %d: %s\n", i, tokens[i]);
31}
Security: Why gets() Is Banned and strcpy() Is Dangerous

Buffer overflow attacks are the most common security vulnerability in C programs. They happen when string operations write past the end of an array. The gets() function was so dangerous it was removed from the C standard entirely. Many other functions are equally hazardous when used carelessly.

DangerousSafe AlternativeNotes
gets()fgets()gets() removed from C11
strcpy()strlcpy() / strncpy()strncpy needs manual null-termination
strcat()strlcat() / strncat()strncat needs careful size math
sprintf()snprintf()snprintf always null-terminates
scanf("%s")fgets() or scanf("%49s")Width specifier limits input
string_security.c
C
1// VULNERABLE: buffer overflow with strcpy
2char small[8];
3strcpy(small, "This string is way too long!");
4// Writes 29 bytes into 8-byte buffer — corrupts the stack
5// An attacker can overwrite the return address and execute shellcode
6
7// SAFE: use strlcpy with proper bounds
8char safe[8];
9strlcpy(safe, "This string is way too long!", sizeof(safe));
10// Safely truncated to "This st" — always null-terminated
11
12// VULNERABLE: sprintf without bounds
13char buf[20];
14sprintf(buf, "User: %s, ID: %d", very_long_username, id);
15// If output exceeds 20 bytes, buffer overflow
16
17// SAFE: always use snprintf
18char safe_buf[20];
19snprintf(safe_buf, sizeof(safe_buf), "User: %s, ID: %d",
20 very_long_username, id);
21// Safely truncated and null-terminated

info

Compile with -Wall -Wextra -Werror to catch many string-related bugs at compile time. Use -fsanitize=address at compile and link time to detect buffer overflows at runtime. Consider using static analysis tools like cppcheck or clang-tidy.
Common Pitfalls
PitfallConsequencePrevention
Missing null terminatorReads past buffer, garbage outputAlways initialize char arrays with = `{0}`
Modifying string literalsSegfault or silent corruptionUse const char * for literals
Comparing with ==Compares addresses, not contentUse strcmp()
Forgetting strncpy null-terminationReads garbage past bufferAlways manually add `\0`
Using gets()Guaranteed buffer overflow possibleUse fgets() instead
string_pitfalls.c
C
1// Pitfall: comparing strings with ==
2char a[] = "Hello";
3char b[] = "Hello";
4if (a == b) {
5 // FALSE — different addresses, same content
6 // Compilers may optimize identical literals to one address
7}
8
9// Correct way
10if (strcmp(a, b) == 0) {
11 printf("Equal!\n");
12}
13
14// Pitfall: forgot null terminator after manual construction
15char raw[5];
16raw[0] = 'H'; raw[1] = 'e'; raw[2] = 'l'; raw[3] = 'l'; raw[4] = 'o';
17// printf("%s", raw); // UNDEFINED — no \0
18// Fix: char raw[6] = "Hello"; or add raw[5] = '\0';
Quick Reference
FunctionPurposeSafe Version
strlen()String length (no null)
strcpy()Copy stringstrlcpy()
strcat()Concatenate stringsstrlcat()
strcmp()Lexicographic compare
strchr() / strrchr()Find character in string
strstr()Find substring
sprintf()Format to stringsnprintf()
strtok()Split into tokensstrtok_r()
atoi() / atof()String to numberstrtol() / strtod()
$Blueprint — Engineering Documentation·Section ID: C-STRINGS·Revision: 1.0