Appearance
C Strings, Bytes, and Text Encodings
C does not have one owning string value type. Most traditional interfaces use a pointer to a null-terminated sequence of char, while robust programs also pass explicit lengths for buffers, slices, files, and network data.
A string literal creates an array
c
const char *name = "Ada";The literal contains {'A', 'd', 'a', '\0'}. Modifying a string literal is undefined behavior, so point to it through const char *. An initialized array creates writable storage:
c
char name[] = "Ada";
name[0] = 'E';strlen counts bytes before the first null byte; it does not include the terminator and does not count user-perceived characters.
Buffers need capacity and current length
An API that writes text should know destination capacity:
c
int format_label(char *destination, size_t capacity, int value);snprintf reports the length that would have been written, allowing truncation detection:
c
#include <stdio.h>
int main(void) {
char buffer[16];
int needed = snprintf(buffer, sizeof buffer, "item-%d", 42);
if (needed < 0 || (size_t)needed >= sizeof buffer) {
return 1;
}
puts(buffer);
return 0;
}Functions such as strcpy require the caller to prove sufficient capacity. Bounded functions are not automatically safe: some have surprising padding or termination rules. Learn each contract and keep size calculations beside the allocation or array.
Bytes are not characters
UTF-8 represents one Unicode scalar value with one to four bytes. When the execution encoding is UTF-8, strlen("é") commonly reports two rather than one, and grapheme clusters can contain multiple scalar values. ISO C does not require the execution character set to be UTF-8, so even that byte count is an environment assumption.
C's execution character sets, multibyte functions, wide characters, locale state, and newer UTF-oriented types form a complicated portability area. A practical cross-platform policy is often:
- define external text as UTF-8;
- keep byte length explicit;
- use a proven Unicode library for normalization, segmentation, or case mapping;
- convert deliberately at Windows UTF-16 API boundaries;
- never assume one
charequals one displayed character.
Parsing should report boundaries and errors
atoi cannot distinguish invalid input from a valid zero and does not expose overflow. strtol provides an end pointer and error signaling:
c
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
static int parse_int(const char *text, int *result) {
char *end = NULL;
errno = 0;
long value = strtol(text, &end, 10);
if (text == end || *end != '\0' || errno == ERANGE ||
value < INT_MIN || value > INT_MAX) {
return 0;
}
*result = (int)value;
return 1;
}External bytes are untrusted even when a type cast or terminator makes them convenient to access. Validate length before searching for a terminator.
Binary data should use byte-oriented contracts
An arbitrary byte buffer may contain zeros and is not a C string. Represent it with a pointer plus length, often using unsigned char * or uint8_t * where available and appropriate. File and socket reads report the number of bytes actually received; they do not append a terminator unless the program does so within allocated capacity.
Optional prompts
Predict: Does strlen include the terminating null byte?
Answer: No. It counts preceding bytes. Storage for a copied C string generally needs
strlen(source) + 1bytes.
Explain: Why is UTF-8 text not safely indexed by user-visible character using text[i]?
Answer: The index selects one byte. Unicode scalar values may span several bytes, and a displayed grapheme may span several scalar values.