Strings
C does not have a string type. What the language calls a string is a contiguous sequence of char objects terminated by a null byte (the byte with value zero, written '\0'). String literals — sequences of characters between double quotes — are stored as static arrays of char and decay to char * in most contexts. The library functions that operate on strings are declared in <string.h> and use the null-terminator convention exclusively. The conventions and pitfalls of working with strings in C — buffer-sizing, lifetime management, encoding handling — derive from this representational choice and account for a substantial portion of the bugs found in C programs that handle text.
String literals
A string literal is a sequence of characters in double quotes. The literal denotes an array of char of length one greater than the number of characters in the source — the trailing null byte is included automatically:
"hello" // a 6-element array: 'h','e','l','l','o','\0'
"" // a 1-element array: '\0'
Adjacent string literals are concatenated by the preprocessor — a useful idiom for long literals:
const char *banner =
"+-----------------------------+\n"
"| polyglot reference |\n"
"+-----------------------------+\n";
String literals are stored in static, typically read-only memory. Modifying a string literal is undefined behaviour:
char *s = "hello";
s[0] = 'H'; // undefined behaviour: writing to a string literal
char arr[] = "hello";
arr[0] = 'H'; // legal: arr is a writable copy of the literal
C23 promoted string literals from char[N] to const char[N] for type-safety reasons; the conversion to char * for compatibility remains.
Escape sequences
Within a string or character literal, the following escape sequences denote characters that cannot be written directly:
| Escape | Meaning |
|---|---|
\n | newline |
\t | horizontal tab |
\r | carriage return |
\0 | null character |
\\ | backslash |
\' | apostrophe |
\" | double quote |
\xHH | the byte with hexadecimal value HH |
\NNN | the byte with octal value NNN |
\uXXXX | the universal character with the named code point (since C99) |
The hexadecimal escape consumes as many hexadecimal digits as appear; the conventional form "\x0Ahello" is unambiguous because h is not a hex digit, but "\x0A1" is '\n', '1' only because A and 1 form a valid hex pair — care is warranted.
Character literals
A character literal in single quotes denotes a value of type int:
int c = 'A'; // c == 65 on an ASCII platform
char ch = 'A'; // implicit conversion from int to char
The type discrepancy is historical: characters were promoted to int in early C, and the standard preserves the rule. In practice, the implicit conversion to char makes the distinction nearly invisible.
The four prefixed forms are:
L'A'— wide character (wchar_t).u'A'— 16-bit Unicode character (char16_t, since C11).U'A'— 32-bit Unicode character (char32_t, since C11).u8'A'— UTF-8 character (char8_tsince C23,unsigned charbefore).
The null-terminator convention
A string in C is a sequence of bytes whose end is marked by a byte with value zero. Almost every standard-library function that takes a string takes a pointer to the first byte and discovers the length by scanning until it finds the terminator. The convention has two consequences that distinguish C strings from those in most other languages:
- The length is not stored; computing it is a linear-time operation.
- A string cannot contain an interior null byte. Any data that may include zero bytes — binary file contents, network packets — must be carried as a
(pointer, length)pair, not a string.
The standard library’s binary-data routines (memcpy, memcmp, memset) take an explicit length and have no opinion about null bytes; these are the routines to use for arbitrary byte sequences.
The <string.h> family
The principal string functions are:
| Function | Effect |
|---|---|
strlen(s) | Returns the length of s, not counting the terminator. |
strcpy(dst, src) | Copies src to dst, including the terminator. |
strncpy(dst, src, n) | Copies up to n bytes; pads with null if src is shorter; does not terminate if src is longer. |
strcat(dst, src) | Appends src to dst. |
strncat(dst, src, n) | Appends up to n bytes of src; always terminates. |
strcmp(a, b) | Returns negative, zero, or positive as a compares less than, equal to, or greater than b. |
strncmp(a, b, n) | As strcmp, but examines at most n bytes. |
strchr(s, c) | Returns a pointer to the first occurrence of c in s, or NULL. |
strrchr(s, c) | As strchr, but the last occurrence. |
strstr(haystack, needle) | Returns a pointer to the first occurrence of needle in haystack, or NULL. |
strspn(s, set) | Length of the initial segment of s consisting only of characters in set. |
strcspn(s, set) | Length of the initial segment of s consisting of characters not in set. |
strpbrk(s, set) | Pointer to the first character in s that appears in set, or NULL. |
strtok(s, sep) | Tokenises s in place; uses internal state — not reentrant. |
strerror(errno) | A textual description of the error number. |
The bytewise counterparts in <string.h> (memcpy, memmove, memcmp, memset, memchr) take an explicit length and operate on void *; they are the appropriate primitive for non-string byte data.
Buffer sizing
Functions that write to a buffer require the caller to ensure the buffer is large enough. strcpy(dst, src) will write past the end of dst if dst is shorter than strlen(src) + 1, and the resulting buffer overflow is the most-cited single class of security vulnerability in C programs. The conventional defences are:
- Use
snprintfrather thanstrcpy/strcatfor general-purpose string building.snprintftakes the destination size and always terminates. - When
strncpymust be used, terminate explicitly:strncpy(dst, src, n - 1); dst[n - 1] = '\0'; - Track buffer sizes alongside buffer pointers; never rely on a function to “know” the size.
char buf[64];
int n = snprintf(buf, sizeof buf, "%s = %d\n", name, value);
if (n < 0 || (size_t)n >= sizeof buf) {
/* truncation or error; handle */
}
snprintf returns the number of bytes that would have been written had the buffer been large enough; a return value greater than or equal to the buffer size signals truncation.
Formatted I/O
The printf family in <stdio.h> constructs formatted strings from a format string and a variable number of arguments. The format string contains literal characters and conversion specifications of the form %[flags][width][.precision][length]conversion:
printf("%s is %d years old\n", name, age);
printf("%10.3f\n", 3.14159); // right-justified in 10 columns, 3 fractional digits
printf("%-10s|%6d\n", label, count); // left-justified, width 10
printf("0x%08lx\n", (unsigned long)addr);
The conversion characters are:
%d,%i— signed decimal integer%u— unsigned decimal integer%o,%x,%X— unsigned octal, lowercase hex, uppercase hex%f,%e,%E,%g,%G— floating-point in fixed, scientific, or general form%c— single character%s— null-terminated string%p— pointer (implementation-defined format)%%— literal%
Length modifiers — h, hh, l, ll, j, z, t, L — adapt the conversion to types other than the default. Notably, %zu for size_t (since C99) and %td for ptrdiff_t should be preferred over casting to unsigned long.
A mismatched format string and argument list is undefined behaviour. The compiler can verify the match for printf-family calls when given an enabled diagnostic (-Wformat in GCC and Clang); compiling with this warning treated as an error catches many such mismatches at compile time.
The scanf family parses input strings into typed values. Its rules are subtler than printf’s and its error reporting is awkward; for production parsing, fgets followed by manual or strtol-based scanning is generally easier to make correct.
String building beyond the library
The standard <string.h> is functional but minimal. Two patterns recur for non-trivial string work:
A growable buffer
typedef struct {
char *data;
size_t len;
size_t cap;
} string_builder;
bool sb_append(string_builder *sb, const char *src) {
size_t addlen = strlen(src);
if (sb->len + addlen + 1 > sb->cap) {
size_t newcap = sb->cap ? sb->cap : 16;
while (newcap < sb->len + addlen + 1) newcap *= 2;
char *p = realloc(sb->data, newcap);
if (!p) return false;
sb->data = p;
sb->cap = newcap;
}
memcpy(sb->data + sb->len, src, addlen + 1);
sb->len += addlen;
return true;
}
A tokenising loop with strchr
const char *p = input;
while (*p) {
const char *end = strchr(p, ',');
size_t len = end ? (size_t)(end - p) : strlen(p);
handle(p, len);
p = end ? end + 1 : p + len;
}
strtok exists for the same purpose but maintains internal state across calls and is therefore not reentrant; strtok_r (POSIX) takes a state pointer and is preferred where reentrancy matters.
Wide and Unicode strings
The standard provides three wider character types and corresponding string-handling functions:
wchar_tand<wchar.h>(since C95). The size ofwchar_tis implementation-defined: 16 bits on Windows, 32 bits on Unix. Wide-character strings are not portably interchangeable.char16_t,char32_tand<uchar.h>(since C11). UTF-16 and UTF-32 representations with portable widths.- UTF-8 in
chararrays, written asu8"…"literals (since C11).
In contemporary code the conventional approach to text encoding is:
- Use
chararrays containing UTF-8 internally. - Use
<uchar.h>conversion functions or a third-party library when transcoding between UTF-8 and the platform’s native wide encoding.
The Unicode-aware classification and case-mapping operations (full case folding, normalisation, segmentation) are not in the C standard library; programs that need them link against ICU, libunistring, or platform APIs.