C strings
last modified November 14, 2026
C has no string type. A string is an array of char that holds a
sequence of characters followed by a null character, '\0', and
every string function in the standard library finds the end of the text by
looking for that terminator. This tutorial covers character arrays and string
literals, the difference between sizeof and strlen,
copying, joining, comparing, searching and splitting strings, reading lines of
input, converting between strings and numbers, and the buffer mistakes that make
string handling the most common source of bugs in C.
A string is a character array
A string literal in double quotes initializes a char array. The
compiler stores the characters one per element and adds the terminating null
character, so the three-letter word below takes four bytes. The next program
prints every element of the array with its character code.
#include <stdio.h>
int main(void) {
char word[] = "cat";
for (size_t i = 0; i < sizeof word; i++) {
if (word[i] != '\0') {
printf("word[%zu] = '%c' (%d)\n", i, word[i], word[i]);
} else {
printf("word[%zu] = '\\0' (%d)\n", i, word[i]);
}
}
printf("%s\n", word);
return 0;
}
sizeof word is the size of the whole array, 4, so the loop also
visits the last element. The terminator is the character with code 0, written
'\0'; it is not the digit '0', whose code is 48. The
%s conversion of printf prints characters starting at
the address it is given and stops at the first null character.
$ gcc -std=c17 -Wall -Wextra -o char_array char_array.c $ ./char_array word[0] = 'c' (99) word[1] = 'a' (97) word[2] = 't' (116) word[3] = '\0' (0) cat
sizeof versus strlen
sizeof gives the size of an array in bytes and is computed by the
compiler. strlen from <string.h> counts the
characters before the first null character at run time. The two answer
different questions and are equal only by accident.
#include <stdio.h>
#include <string.h>
int main(void) {
char name[20] = "Lucia";
char text[] = "one\0two";
printf("sizeof name: %zu\n", sizeof name);
printf("strlen name: %zu\n", strlen(name));
printf("sizeof text: %zu\n", sizeof text);
printf("strlen text: %zu\n", strlen(text));
printf("text: %s\n", text);
printf("second part: %s\n", text + 4);
return 0;
}
The array name has room for 20 characters but holds a five-letter
string; the unused elements are set to zero. The literal
"one\0two" contains an embedded null character, so its array has 8
bytes (seven characters plus the terminator the compiler adds), but
strlen and %s stop after one. The text
after the embedded terminator is still in memory and is reached through the
address text + 4.
$ gcc -std=c17 -Wall -Wextra -o sizeof_strlen sizeof_strlen.c $ ./sizeof_strlen sizeof name: 20 strlen name: 5 sizeof text: 8 strlen text: 3 text: one second part: two
Initializing character arrays
A character array can be initialized from a string literal or from a list of characters. When the size is given explicitly, the remaining elements are filled with zeros. When the size is exactly the number of characters, C quietly drops the terminator, which is legal C but leaves an array that is not a string.
#include <stdio.h>
int main(void) {
char a[] = "sky";
char b[] = {'s', 'k', 'y', '\0'};
char c[8] = "sky";
char d[3] = "sky";
printf("a: %s (%zu bytes)\n", a, sizeof a);
printf("b: %s (%zu bytes)\n", b, sizeof b);
printf("c: %s (%zu bytes)\n", c, sizeof c);
printf("c[5] = %d\n", c[5]);
printf("d: %.3s (%zu bytes)\n", d, sizeof d);
return 0;
}
Arrays a and b are identical; the brace form must list
the '\0' explicitly. c has eight elements, so elements
3 to 7 are zero. d has room for the three letters only, and C
accepts the initializer without the terminator (C++ rejects it). Printing it
with plain %s would read past the end of the array, so the program
uses the precision %.3s, which prints at most three characters.
$ gcc -std=c17 -Wall -Wextra -o init init.c $ ./init a: sky (4 bytes) b: sky (4 bytes) c: sky (8 bytes) c[5] = 0 d: sky (3 bytes)
Arrays and pointers to literals
A string literal itself is a static array that exists for the whole run of the
program. Initializing a char array copies the literal into the
array, while initializing a pointer only stores the address of the literal.
#include <stdio.h>
int main(void) {
char arr[] = "falcon";
const char *ptr = "falcon";
arr[0] = 'F';
printf("arr: %s, sizeof arr: %zu\n", arr, sizeof arr);
printf("ptr: %s, sizeof ptr: %zu\n", ptr, sizeof ptr);
ptr = "eagle";
printf("ptr: %s\n", ptr);
return 0;
}
arr is a seven-byte array owned by main, and its
characters can be changed. ptr is a pointer; sizeof
gives the size of the pointer, 8 bytes on a 64-bit system, not the length of the
text. The pointer can be redirected to another literal, but the characters it
points to must not be modified, which is why it is declared as a pointer to
const char.
$ gcc -std=c17 -Wall -Wextra -o literals literals.c $ ./literals arr: Falcon, sizeof arr: 7 ptr: falcon, sizeof ptr: 8 ptr: eagle
Modifying a string literal
For historical reasons the type of a string literal in C is char[],
not const char[], so a plain char * can point to it
without a cast. Writing through such a pointer is undefined behavior.
#include <stdio.h>
int main(void) {
char *word = "falcon";
word[0] = 'F';
printf("%s\n", word);
return 0;
}
GCC places literals in a read-only segment, so the assignment to
word[0] crashes the program with a segmentation fault. The
-Wwrite-strings option gives literals the type
const char[] for the purpose of diagnostics, and the compiler then
reports the dangerous initialization. Declaring the pointer as
const char * turns the write into a compile-time error; declaring
an array instead gives a modifiable copy.
$ gcc -std=c17 -Wall -Wextra -o modify_literal modify_literal.c $ ./modify_literal Segmentation fault (core dumped)
$ gcc -std=c17 -Wall -Wextra -Wwrite-strings -o modify_literal modify_literal.c
modify_literal.c: In function ‘main’:
modify_literal.c:5:18: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
5 | char *word = "falcon";
| ^~~~~~~~
Escape sequences
Characters that cannot be typed directly in a literal are written as escape sequences starting with a backslash. Adjacent string literals are joined into one by the compiler, which is the usual way to split a long literal over several source lines.
#include <stdio.h>
int main(void) {
printf("tab:\t|\n");
printf("quote: \"C\" and backslash: \\\n");
printf("apostrophe: %c\n", '\'');
printf("hex: \x41\x42\x43, octal: \101\102\103\n");
printf("percent sign: 100%%\n");
const char *msg = "This literal is split "
"over two lines "
"in the source.";
printf("%s\n", msg);
return 0;
}
\t is a tab, \" a double quote inside a string,
\\ a backslash and \' an apostrophe in a character
constant. \x41 gives a character by its hexadecimal code and
\101 by its octal code; both are the letter A. The percent sign is
not an escape sequence of the language but a conversion character of
printf, so a literal percent is written %%. The three
pieces of msg form a single literal with one terminator.
$ gcc -std=c17 -Wall -Wextra -o escapes escapes.c $ ./escapes tab: | quote: "C" and backslash: \ apostrophe: ' hex: ABC, octal: ABC percent sign: 100% This literal is split over two lines in the source.
Looping over characters
Because the end of a string is marked by the terminator, a loop over its
characters runs until it reaches '\0'; the length does not need to
be computed first. The loop can use an index or move a pointer along the string.
#include <stdio.h>
int main(void) {
const char *text = "an old falcon";
int vowels = 0;
for (size_t i = 0; text[i] != '\0'; i++) {
switch (text[i]) {
case 'a': case 'e': case 'i': case 'o': case 'u':
vowels++;
break;
}
}
printf("vowels: %d\n", vowels);
for (const char *p = text; *p; p++) {
putchar(*p == ' ' ? '_' : *p);
}
putchar('\n');
return 0;
}
The first loop counts vowels with an index. The second loop advances a pointer
p and stops when *p is the null character, which is
false in a condition; *p and *p != '\0' mean the same.
Calling strlen in the condition of a loop, as in
i < strlen(text), scans the whole string on every iteration and
turns a linear loop into a quadratic one.
$ gcc -std=c17 -Wall -Wextra -o iterate iterate.c $ ./iterate vowels: 4 an_old_falcon
Writing strlen
Implementing strlen shows how little a C string carries: the only
way to know its length is to walk to the terminator. The function below does
that with a pointer and returns the distance from the start.
#include <stdio.h>
#include <string.h>
static size_t my_strlen(const char *s) {
const char *p = s;
while (*p != '\0') {
p++;
}
return (size_t) (p - s);
}
int main(void) {
const char *words[] = { "", "a", "sky", "programming" };
for (size_t i = 0; i < sizeof words / sizeof words[0]; i++) {
printf("\"%s\": my_strlen %zu, strlen %zu\n", words[i],
my_strlen(words[i]), strlen(words[i]));
}
return 0;
}
The parameter is const char * because the function only reads the
string. Subtracting two pointers into the same array gives the number of
elements between them, and the result is converted to size_t, the
type strlen returns. The empty string "" is a valid
string of length 0 that consists of the terminator alone.
$ gcc -std=c17 -Wall -Wextra -o my_strlen my_strlen.c $ ./my_strlen "": my_strlen 0, strlen 0 "a": my_strlen 1, strlen 1 "sky": my_strlen 3, strlen 3 "programming": my_strlen 11, strlen 11
Copying strings
Arrays cannot be assigned with =, so a string is copied with a
function. strcpy copies the characters and the terminator but
knows nothing about the size of the destination; the caller has to check it.
snprintf takes the size of the buffer, never writes past it and
always terminates the result.
#include <stdio.h>
#include <string.h>
int main(void) {
char dest[16];
const char *src = "blue sky";
if (strlen(src) < sizeof dest) {
strcpy(dest, src);
printf("strcpy: %s\n", dest);
}
char small[6];
int n = snprintf(small, sizeof small, "%s", src);
printf("snprintf: \"%s\"\n", small);
if ((size_t) n >= sizeof small) {
printf("truncated: needed %d characters, room for %zu\n",
n, sizeof small - 1);
}
return 0;
}
The check before strcpy uses <, not
<=, because the destination needs one byte more than the length
for the terminator. snprintf writes at most
sizeof small - 1 characters and returns the length the complete
result would have had, so a return value equal to or greater than the buffer
size reveals truncation. GCC notices the truncation at compile time and warns
about it; here it is intentional.
$ gcc -std=c17 -Wall -Wextra -o copy copy.c
copy.c: In function ‘main’:
copy.c:15:44: warning: ‘%s’ directive output truncated writing 8 bytes into a region of size 6 [-Wformat-truncation=]
15 | int n = snprintf(small, sizeof small, "%s", src);
| ^~
copy.c:15:13: note: ‘snprintf’ output 9 bytes into a destination of size 6
15 | int n = snprintf(small, sizeof small, "%s", src);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ ./copy
strcpy: blue sky
snprintf: "blue "
truncated: needed 8 characters, room for 5
The strncpy trap
strncpy looks like a bounded strcpy, but it was
designed for fixed-width fields, not for strings. When the source is at least as
long as the limit, it copies exactly that many characters and does not add a
terminator.
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[4];
strncpy(buf, "abc", sizeof buf);
printf("short source: %s\n", buf);
strncpy(buf, "abcdef", sizeof buf);
buf[sizeof buf - 1] = '\0';
printf("long source: %s\n", buf);
return 0;
}
The first call copies abc and pads the rest of the buffer with
zeros. The second copies abcd and leaves the buffer unterminated;
the line that stores '\0' in the last element repairs it, at the
price of cutting the text to three characters. Without that line the following
printf would read beyond the array. snprintf is
usually the simpler choice.
$ gcc -std=c17 -Wall -Wextra -o strncpy_trap strncpy_trap.c $ ./strncpy_trap short source: abc long source: abc
Joining strings
strcat appends a string to the end of another. It must first find
the end of the destination, and like strcpy it does not know how
much room is left. Building a string with snprintf and a running
offset avoids both problems.
#include <stdio.h>
#include <string.h>
int main(void) {
char path[32] = "/home";
strcat(path, "/");
strcat(path, "lucia");
printf("%s\n", path);
const char *parts[] = { "red", "green", "blue", "yellow" };
char line[20];
size_t used = 0;
line[0] = '\0';
for (size_t i = 0; i < sizeof parts / sizeof parts[0]; i++) {
int n = snprintf(line + used, sizeof line - used, "%s%s",
i > 0 ? "," : "", parts[i]);
if (n < 0 || (size_t) n >= sizeof line - used) {
line[used] = '\0';
printf("buffer full after %zu words\n", i);
break;
}
used += (size_t) n;
}
printf("%s (%zu characters)\n", line, used);
return 0;
}
The path is built with two strcat calls into a buffer that is known
to be large enough. The second part keeps the number of characters written so
far in used, so each call starts at the end of the text instead of
searching for it, and is told how much space remains. When a word no longer
fits, snprintf has already written a truncated piece; restoring the
terminator at line[used] removes it, and the loop stops with the
three words that fit.
$ gcc -std=c17 -Wall -Wextra -o concat concat.c $ ./concat /home/lucia buffer full after 3 words red,green,blue (14 characters)
Comparing strings
The == operator compares addresses, not contents. Two arrays with
the same text are different objects, so they never compare equal.
strcmp compares two strings character by character and returns a
negative number, zero or a positive number.
#include <stdio.h>
#include <string.h>
int main(void) {
char a[] = "apple";
char b[] = "apple";
printf("a == b: %d\n", a == b);
printf("strcmp(a, b): %d\n", strcmp(a, b));
const char *words[] = { "apple", "banana", "Apple", "app" };
for (size_t i = 1; i < sizeof words / sizeof words[0]; i++) {
int r = strcmp(words[0], words[i]);
const char *rel = r < 0 ? "before" : r > 0 ? "after" : "equal to";
printf("\"%s\" sorts %s \"%s\"\n", words[0], rel, words[i]);
}
printf("first 3 equal: %d\n", strncmp("apple", "application", 3) == 0);
return 0;
}
GCC warns about the comparison of two arrays because it is almost always a
mistake. strcmp compares the character codes as
unsigned char, so uppercase letters, whose codes are smaller, sort
before lowercase ones, and a prefix sorts before the longer string. Only the
sign of the result is specified, not its value. strncmp compares at
most the given number of characters, which is useful for testing a prefix.
$ gcc -std=c17 -Wall -Wextra -o compare compare.c
compare.c: In function ‘main’:
compare.c:9:30: warning: comparison between two arrays [-Warray-compare]
9 | printf("a == b: %d\n", a == b);
| ^~
compare.c:9:30: note: use ‘&a[0] == &b[0]’ to compare the addresses
$ ./compare
a == b: 0
strcmp(a, b): 0
"apple" sorts before "banana"
"apple" sorts after "Apple"
"apple" sorts after "app"
first 3 equal: 1
Reading lines
fgets reads one line into a buffer of a given size and keeps the
newline character. strcspn returns the position of the first
newline, or the length of the string if there is none, so storing a terminator
at that position removes the newline in both cases.
#include <stdio.h>
#include <string.h>
int main(void) {
char line[64];
while (fgets(line, sizeof line, stdin) != NULL) {
line[strcspn(line, "\n")] = '\0';
if (line[0] == '\0') {
continue;
}
printf("[%s] has %zu characters\n", line, strlen(line));
}
return 0;
}
fgets returns NULL at the end of input, which ends the
loop. A line longer than 63 characters would be returned in several pieces, and
only the last piece would contain the newline. The empty line in the input is
skipped. The gets function, which had no size parameter, was
removed from the language in C11 and must not be used.
$ gcc -std=c17 -Wall -Wextra -o read_line read_line.c $ printf 'blue sky\n\nan old falcon\n' | ./read_line [blue sky] has 8 characters [an old falcon] has 13 characters
Classifying and converting characters
The functions in <ctype.h> test a character for a class,
such as letter, digit or white space, and convert between upper and lower case.
Their argument must be representable as unsigned char or be
EOF.
#include <ctype.h>
#include <stdio.h>
static void to_upper(char *s) {
for (; *s; s++) {
*s = (char) toupper((unsigned char) *s);
}
}
int main(void) {
char msg[] = "Hello there, 42 times!";
int letters = 0, digits = 0, spaces = 0;
for (const char *p = msg; *p; p++) {
unsigned char c = (unsigned char) *p;
if (isalpha(c)) letters++;
else if (isdigit(c)) digits++;
else if (isspace(c)) spaces++;
}
printf("letters: %d, digits: %d, spaces: %d\n", letters, digits, spaces);
to_upper(msg);
printf("%s\n", msg);
return 0;
}
On x86 and many other platforms char is signed, and a byte above 127, which
appears in any non-ASCII text, would be passed to the functions as a negative
number. That is undefined behavior, so the program casts every character to
unsigned char first. to_upper changes the string in
place, which works because msg is an array and not a pointer to a
literal.
$ gcc -std=c17 -Wall -Wextra -o case case.c $ ./case letters: 15, digits: 2, spaces: 3 HELLO THERE, 42 TIMES!
Arrays of strings
A list of strings can be stored as an array of pointers to the strings, or as a two-dimensional array of characters with one row per string. The first form is compact and suits read-only text; the second gives every string a modifiable buffer of the same size.
#include <stdio.h>
int main(void) {
const char *ptrs[] = { "sun", "moon", "constellation" };
char grid[][14] = { "sun", "moon", "constellation" };
printf("sizeof ptrs: %zu\n", sizeof ptrs);
printf("sizeof grid: %zu\n", sizeof grid);
size_t n = sizeof grid / sizeof grid[0];
for (size_t i = 0; i < n; i++) {
printf("%-14s %s\n", ptrs[i], grid[i]);
}
grid[1][0] = 'M';
printf("modified: %s\n", grid[1]);
return 0;
}
ptrs holds three 8-byte pointers to literals stored elsewhere.
grid holds three rows of 14 bytes, the size needed by the longest
word with its terminator, and its 42 bytes contain copies of the text.
The rows of grid can be modified; the literals that
ptrs points to cannot.
$ gcc -std=c17 -Wall -Wextra -o string_arrays string_arrays.c $ ./string_arrays sizeof ptrs: 24 sizeof grid: 42 sun sun moon moon constellation constellation modified: Moon
Searching
strchr returns a pointer to the first occurrence of a character,
strrchr to the last, and strstr to the first
occurrence of a substring. All three return NULL when there is no
match.
#include <stdio.h>
#include <string.h>
int main(void) {
const char *email = "lucia.novak@example.com";
const char *at = strchr(email, '@');
const char *dot = strrchr(email, '.');
if (at != NULL) {
printf("user: %.*s\n", (int) (at - email), email);
printf("domain: %s\n", at + 1);
}
if (dot != NULL) {
printf("top-level domain: %s\n", dot + 1);
}
const char *text = "the cat sat on the mat with the hat";
int count = 0;
for (const char *p = text; (p = strstr(p, "the")) != NULL; p++) {
printf("\"the\" at offset %td\n", p - text);
count++;
}
printf("found %d times\n", count);
return 0;
}
The difference between the pointer returned by strchr and the start
of the string is the length of the user name; the precision in
%.*s takes it as an int argument and prints only that
many characters. The domain starts one character after the at sign. The loop
finds all occurrences of the by restarting the search one character
after each match; %td is the conversion for
ptrdiff_t, the type of a pointer difference.
$ gcc -std=c17 -Wall -Wextra -o search search.c $ ./search user: lucia.novak domain: example.com top-level domain: com "the" at offset 0 "the" at offset 15 "the" at offset 28 found 3 times
Splitting into tokens
strtok splits a string into tokens separated by any of the
characters in a delimiter set. The first call receives the string, later calls
receive NULL and continue where the previous one stopped.
#include <stdio.h>
#include <string.h>
int main(void) {
char data[] = "id=7; name=falcon;speed=389 ; ";
int n = 0;
for (char *tok = strtok(data, "; "); tok != NULL;
tok = strtok(NULL, "; ")) {
char *eq = strchr(tok, '=');
if (eq != NULL) {
*eq = '\0';
printf("%d: key=%s value=%s\n", ++n, tok, eq + 1);
}
}
return 0;
}
strtok writes a null character over each delimiter it finds, so it
needs a modifiable array and destroys the original text. Runs of delimiters are
treated as one, and leading and trailing delimiters produce no empty tokens.
Each token is then split at the equals sign in the same way, by overwriting it
with a terminator. Because strtok keeps its position in a hidden
static variable, two tokenizations cannot be interleaved; POSIX
strtok_r takes the position as an extra argument.
$ gcc -std=c17 -Wall -Wextra -o split split.c $ ./split 1: key=id value=7 2: key=name value=falcon 3: key=speed value=389
Strings and numbers
snprintf converts numbers to text with the same format as
printf. In the other direction, strtol parses an
integer and reports where it stopped, which makes it possible to reject input
that is not a number at all, has trailing characters or is out of range.
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
static int parse_int(const char *s, long *out) {
char *end;
errno = 0;
long v = strtol(s, &end, 10);
if (end == s || *end != '\0' || errno == ERANGE) {
return -1;
}
*out = v;
return 0;
}
int main(void) {
char buf[32];
snprintf(buf, sizeof buf, "%d-%.2f-%x", 42, 3.14159, 255);
printf("formatted: %s\n", buf);
const char *inputs[] = { "123", "-17", "12abc", "", "99999999999999999999" };
for (size_t i = 0; i < sizeof inputs / sizeof inputs[0]; i++) {
long v;
if (parse_int(inputs[i], &v) == 0) {
printf("\"%s\" -> %ld\n", inputs[i], v);
} else {
printf("\"%s\" -> invalid\n", inputs[i]);
}
}
return 0;
}
parse_int accepts a string only when strtol consumed at
least one character, stopped at the terminator and did not set
errno to ERANGE. The atoi function would
return 12 for 12abc and 0 for the empty string, with no way to tell
an error from a real zero. The twenty-digit number is larger than
LONG_MAX, so strtol reports a range error.
$ gcc -std=c17 -Wall -Wextra -o numbers numbers.c $ ./numbers formatted: 42-3.14-ff "123" -> 123 "-17" -> -17 "12abc" -> invalid "" -> invalid "99999999999999999999" -> invalid
Building a string on the heap
When the length of a result is not known in advance, the string is allocated
with malloc. The function below joins words with a separator: it
measures the result first, allocates exactly the needed size and then copies the
pieces.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *join(const char *items[], size_t n, const char *sep) {
size_t len = 1;
for (size_t i = 0; i < n; i++) {
len += strlen(items[i]) + (i > 0 ? strlen(sep) : 0);
}
char *res = malloc(len);
if (res == NULL) {
return NULL;
}
char *p = res;
for (size_t i = 0; i < n; i++) {
if (i > 0) {
size_t k = strlen(sep);
memcpy(p, sep, k);
p += k;
}
size_t k = strlen(items[i]);
memcpy(p, items[i], k);
p += k;
}
*p = '\0';
return res;
}
int main(void) {
const char *words[] = { "C", "is", "a", "small", "language" };
char *s = join(words, 5, " ");
if (s == NULL) {
perror("malloc");
return 1;
}
printf("%s (%zu characters)\n", s, strlen(s));
free(s);
return 0;
}
The size starts at 1 for the terminator. The copying uses memcpy
with known lengths and a pointer that marks the end of the result, so no part of
the string is scanned twice. The function returns a pointer the caller owns and
must release with free. Allocation is covered in detail in
C malloc and free.
$ gcc -std=c17 -Wall -Wextra -o join join.c $ ./join C is a small language (21 characters)
Reversing in place
A string in a modifiable array can be changed character by character. Reversing it with two pointers that move toward each other is a short exercise in pointer handling.
#include <stdio.h>
#include <string.h>
static void reverse(char *s) {
size_t n = strlen(s);
if (n < 2) {
return;
}
for (char *l = s, *r = s + n - 1; l < r; l++, r--) {
char tmp = *l;
*l = *r;
*r = tmp;
}
}
int main(void) {
char words[][12] = { "falcon", "level", "a", "" };
for (size_t i = 0; i < sizeof words / sizeof words[0]; i++) {
char copy[12];
strcpy(copy, words[i]);
reverse(words[i]);
printf("\"%s\" -> \"%s\"%s\n", copy, words[i],
strcmp(copy, words[i]) == 0 ? " (palindrome)" : "");
}
return 0;
}
l starts at the first character and r at the last
one, before the terminator, and the loop swaps characters until the pointers
meet. The early return handles the empty string, for which
s + n - 1 would point before the array. A string that equals its
reverse is a palindrome; the comparison uses the saved copy.
$ gcc -std=c17 -Wall -Wextra -o reverse reverse.c $ ./reverse "falcon" -> "noclaf" "level" -> "level" (palindrome) "a" -> "a" (palindrome) "" -> "" (palindrome)
Bytes and characters
A char is one byte, not one character of text. In UTF-8, the
encoding used by Linux terminals and most source files, letters outside ASCII
take two to four bytes, and strlen counts bytes.
#include <stdio.h>
#include <string.h>
static size_t count_code_points(const char *s) {
size_t n = 0;
for (; *s; s++) {
if (((unsigned char) *s & 0xC0) != 0x80) {
n++;
}
}
return n;
}
int main(void) {
const char *words[] = { "cafe", "café", "žltý", "€" };
for (size_t i = 0; i < sizeof words / sizeof words[0]; i++) {
printf("%s: bytes %zu, code points %zu\n", words[i],
strlen(words[i]), count_code_points(words[i]));
}
return 0;
}
Every byte of a multibyte UTF-8 sequence except the first has the bit pattern
10xxxxxx. Counting only the bytes whose two top bits are not
10 therefore counts code points. The letter é takes two bytes, the
Slovak word žltý six and the euro sign three. Functions that index,
truncate or reverse strings byte by byte can cut such sequences in half;
proper Unicode handling needs a library or the wide-character functions.
$ gcc -std=c17 -Wall -Wextra -o utf8 utf8.c $ ./utf8 cafe: bytes 4, code points 4 café: bytes 5, code points 4 žltý: bytes 6, code points 4 €: bytes 3, code points 1
Buffer overflows
The classic string bug is forgetting the byte for the terminator. The word
hello has five letters and needs a six-byte array.
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[5];
strcpy(buf, "hello");
printf("%s\n", buf);
return 0;
}
GCC sees that strcpy writes six bytes into a five-byte array and
warns. At run time nothing guarantees a crash: without instrumentation the
program may print hello and silently corrupt a neighbouring
variable. AddressSanitizer, enabled with -fsanitize=address, stops
the program at the faulty write and names the variable and the source line.
$ gcc -std=c17 -Wall -Wextra -g -fsanitize=address -o overflow overflow.c
overflow.c: In function ‘main’:
overflow.c:8:5: warning: ‘__builtin_memcpy’ writing 6 bytes into a region of size 5 overflows the destination [-Wstringop-overflow=]
8 | strcpy(buf, "hello");
| ^~~~~~~~~~~~~~~~~~~~
overflow.c:6:10: note: destination object ‘buf’ of size 5
6 | char buf[5];
| ^~~
$ ./overflow 2>&1 | head -n 14
=================================================================
==93397==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x78c4e80f0025 at pc 0x7cc4ea929a2d bp 0x7fffa533b380 sp 0x7fffa533ab28
WRITE of size 6 at 0x78c4e80f0025 thread T0
#0 0x7cc4ea929a2c in memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:117
#1 0x000000401209 in main /home/jano/cdemo/overflow.c:8
#2 0x7cc4ea429d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#3 0x7cc4ea429e3f in __libc_start_main_impl ../csu/libc-start.c:392
#4 0x0000004010c4 in _start (/home/jano/cdemo/overflow+0x4010c4)
Address 0x78c4e80f0025 is located in stack of thread T0 at offset 37 in frame
#0 0x000000401195 in main /home/jano/cdemo/overflow.c:4
This frame has 1 object(s):
[32, 37) 'buf' (line 6) <== Memory access at offset 37 overflows this variable
Guidelines
Most string bugs come from a mismatch between the size of a buffer and the length of the text written into it. The rules below keep the two in step.
- Reserve one extra byte for the terminator in every buffer.
- Pass the buffer size together with the buffer to every function that writes into it.
- Prefer
snprintftostrcpy,strcatandsprintf, and check its return value for truncation. - Do not use
strncpyas a safestrcpy; it may leave the result unterminated. - Point to string literals with
const char *and never modify them. - Compare contents with
strcmp, not with==. - Use
fgetsfor input andstrtolfor numbers; nevergets, and avoidatoi. - Cast to
unsigned charbefore calling the<ctype.h>functions. - Remember that
strlencounts bytes, not characters.
Source
cppreference: null-terminated byte strings
cppreference: string literals
cppreference: escape sequences
The GNU C Library: string and array utilities
This tutorial covered strings in C: character arrays and the null terminator,
sizeof and strlen, literals and escape sequences,
copying, joining, comparing, searching and splitting, reading lines, character
classes, conversions between strings and numbers, heap-allocated strings, UTF-8
bytes and buffer overflows. The individual functions are documented in the
reference pages, for example strlen,
snprintf, strcmp,
strtok and fgets, and
the address arithmetic used throughout is explained in
C pointers. Arrays in general are covered in
C arrays.
Author
List all C tutorials.