C — Strings
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.
There are two primary ways to declare a string in C. They look similar but have critical differences in mutability, storage, and lifetime.
| 1 | // Char array (mutable, stored on the stack) |
| 2 | char greeting[] = "Hello"; |
| 3 | // Compiler allocates 6 bytes: H e l l o \0 |
| 4 | greeting[0] = 'h'; // VALID — it's a mutable array |
| 5 | |
| 6 | // Pointer to string literal (immutable, stored in read-only memory) |
| 7 | const char *msg = "Hello"; |
| 8 | msg[0] = 'h'; // UNDEFINED BEHAVIOR — string literals are read-only |
| 9 | |
| 10 | // Array of pointers to string literals |
| 11 | const char *colors[] = {"Red", "Green", "Blue"}; |
| 12 | |
| 13 | // Pointer to dynamically allocated string |
| 14 | char *dynamic = malloc(6); |
| 15 | strcpy(dynamic, "Hello"); |
| 16 | dynamic[0] = 'h'; // VALID — it's heap memory |
warning
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 `\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.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | int 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 | } |
Reading and writing strings is where most security vulnerabilities originate in C. The standard I/O functions range from extremely dangerous to reasonably safe.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | int 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
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.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.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | int 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 | } |
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.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | int 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.strcat() appends one string to another. Like strcpy, it has no bounds checking. strncat() is safer because it limits the number of characters appended.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | int 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 | } |
You cannot compare strings with ==. That compares pointer addresses, not content. Use strcmp() for lexicographic comparison and strncmp() for partial comparison.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | int 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 | } |
C provides several functions to find characters and substrings within a string. These functions return pointers into the original string, not copies.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | int main(void) { |
| 5 | const char *text = "Hello, World! Hello, C!"; |
| 6 | |
| 7 | // strchr — find first occurrence of a character |
| 8 | char *first_h = strchr(text, 'H'); |
| 9 | if (first_h) { |
| 10 | printf("First H at index: %ld\n", first_h - text); // 0 |
| 11 | } |
| 12 | |
| 13 | // strrchr — find last occurrence of a character |
| 14 | char *last_h = strrchr(text, 'H'); |
| 15 | if (last_h) { |
| 16 | printf("Last H at index: %ld\n", last_h - text); // 14 |
| 17 | } |
| 18 | |
| 19 | // strstr — find first occurrence of a substring |
| 20 | char *found = strstr(text, "World"); |
| 21 | if (found) { |
| 22 | printf("Found 'World' at index: %ld\n", found - text); // 7 |
| 23 | } |
| 24 | |
| 25 | // memchr — find a byte in a memory block (binary-safe) |
| 26 | char data[] = {'a', 0, 'b', 'c'}; |
| 27 | char *result = memchr(data, 0, sizeof(data)); |
| 28 | if (result) { |
| 29 | printf("Null byte at index: %ld\n", result - data); // 1 |
| 30 | } |
| 31 | |
| 32 | // Count occurrences |
| 33 | int count = 0; |
| 34 | const char *search = text; |
| 35 | while ((search = strstr(search, "Hello")) != NULL) { |
| 36 | count++; |
| 37 | search += 5; // skip past the found substring |
| 38 | } |
| 39 | printf("'Hello' appears %d times\n", count); // 2 |
| 40 | |
| 41 | return 0; |
| 42 | } |
Converting between strings and numbers is extremely common. C provides functions for this, but they all have important caveats.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | #include <string.h> |
| 4 | |
| 5 | int 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
strtol() over atoi() because strtol provides error detection through errno and the end pointer. Never use sprintf() without bounds — always use snprintf().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.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | int 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 | } |
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.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | int 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 | } |
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.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | |
| 4 | int 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 | } |
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.
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | #include <stdlib.h> |
| 4 | |
| 5 | int 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 | } |
Here are practical string manipulation functions you'll use frequently. Each demonstrates safe memory handling and idiomatic C patterns.
Reverse a String In-Place
| 1 | void 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 |
| 11 | char s[] = "Hello"; |
| 12 | reverse_string(s); |
| 13 | printf("%s\n", s); // "olleH" |
Check Palindrome
| 1 | int 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 |
| 15 | printf("%d\n", is_palindrome("racecar")); // 1 |
| 16 | printf("%d\n", is_palindrome("hello")); // 0 |
Count Words in a String
| 1 | int 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 |
| 18 | printf("Words: %d\n", count_words(" Hello World ")); // 2 |
| 19 | printf("Words: %d\n", count_words("Single")); // 1 |
Trim Whitespace
| 1 | void 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 |
| 22 | char text[] = " Hello, World! "; |
| 23 | trim(text); |
| 24 | printf("[%s]\n", text); // [Hello, World!] |
Split String by Delimiter
| 1 | #include <stdio.h> |
| 2 | #include <string.h> |
| 3 | #include <stdlib.h> |
| 4 | |
| 5 | int 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 |
| 27 | char tokens[10][64]; |
| 28 | int n = split_string("apple,banana,cherry", ',', tokens, 10); |
| 29 | for (int i = 0; i < n; i++) { |
| 30 | printf("Token %d: %s\n", i, tokens[i]); |
| 31 | } |
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.
| Dangerous | Safe Alternative | Notes |
|---|---|---|
| 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 |
| 1 | // VULNERABLE: buffer overflow with strcpy |
| 2 | char small[8]; |
| 3 | strcpy(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 |
| 8 | char safe[8]; |
| 9 | strlcpy(safe, "This string is way too long!", sizeof(safe)); |
| 10 | // Safely truncated to "This st" — always null-terminated |
| 11 | |
| 12 | // VULNERABLE: sprintf without bounds |
| 13 | char buf[20]; |
| 14 | sprintf(buf, "User: %s, ID: %d", very_long_username, id); |
| 15 | // If output exceeds 20 bytes, buffer overflow |
| 16 | |
| 17 | // SAFE: always use snprintf |
| 18 | char safe_buf[20]; |
| 19 | snprintf(safe_buf, sizeof(safe_buf), "User: %s, ID: %d", |
| 20 | very_long_username, id); |
| 21 | // Safely truncated and null-terminated |
info
-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.| Pitfall | Consequence | Prevention |
|---|---|---|
| Missing null terminator | Reads past buffer, garbage output | Always initialize char arrays with = `{0}` |
| Modifying string literals | Segfault or silent corruption | Use const char * for literals |
| Comparing with == | Compares addresses, not content | Use strcmp() |
| Forgetting strncpy null-termination | Reads garbage past buffer | Always manually add `\0` |
| Using gets() | Guaranteed buffer overflow possible | Use fgets() instead |
| 1 | // Pitfall: comparing strings with == |
| 2 | char a[] = "Hello"; |
| 3 | char b[] = "Hello"; |
| 4 | if (a == b) { |
| 5 | // FALSE — different addresses, same content |
| 6 | // Compilers may optimize identical literals to one address |
| 7 | } |
| 8 | |
| 9 | // Correct way |
| 10 | if (strcmp(a, b) == 0) { |
| 11 | printf("Equal!\n"); |
| 12 | } |
| 13 | |
| 14 | // Pitfall: forgot null terminator after manual construction |
| 15 | char raw[5]; |
| 16 | raw[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'; |
| Function | Purpose | Safe Version |
|---|---|---|
| strlen() | String length (no null) | — |
| strcpy() | Copy string | strlcpy() |
| strcat() | Concatenate strings | strlcat() |
| strcmp() | Lexicographic compare | — |
| strchr() / strrchr() | Find character in string | — |
| strstr() | Find substring | — |
| sprintf() | Format to string | snprintf() |
| strtok() | Split into tokens | strtok_r() |
| atoi() / atof() | String to number | strtol() / strtod() |