What you'll learn
C has no separate built-in string type. Text is represented by character arrays that end with a zero byte. That convention is efficient and universal in C APIs, but capacity and termination are your responsibility.
By the end, you'll be able to:
- Explain null termination, length, and capacity
- Distinguish writable character arrays from string literals
- Read, compare, search, build, and parse text with checked bounds
- Recognize common string-overflow and truncation risks
The C string model
A C string is a sequence of characters followed by '\0'. Library functions walk until that terminator, so a character array without one is not a valid string.
[0]
[1]
[2]
[3]
[4]
[5]
#include <stdio.h>
#include <string.h>
int main(void) {
char word[6] = {'C', 'o', 'd', 'e', '\0'};
printf("%s\n", word);
printf("length=%zu capacity=%zu\n", strlen(word), sizeof word);
return 0;
}Key idea
length + 1 bytes.Literals & mutability
Initializing an array from a literal copies its characters into writable storage. A pointer can refer directly to a literal, but attempting to modify that literal is undefined behavior—use pointer-to-const to communicate the rule.
#include <stdio.h>
int main(void) {
char editable[] = "hello"; // writable array copy
const char *message = "welcome"; // point at a literal; do not modify
editable[0] = 'H';
printf("%s %s\n", editable, message);
return 0;
}Watch out
char *text = "literal";. It compiles in C for historical reasons but does not make the literal writable.Read text safely
fgets reads at most capacity - 1 characters and adds a terminator when it succeeds. If it reads the newline, that newline becomes part of the string.
#include <stdio.h>
#include <string.h>
int main(void) {
char name[40];
printf("Full name: ");
if (fgets(name, sizeof name, stdin) == NULL) {
return 1;
}
name[strcspn(name, "\n")] = '\0'; // remove newline if present
printf("Hello, %s!\n", name);
return 0;
}Note
The string library
| Function | Purpose | Safety question |
|---|---|---|
strlen | Count characters before the terminator | Is the input definitely terminated? |
strcmp | Lexicographically compare two strings | Are both inputs valid strings? |
strchr / strstr | Find a character or substring | Did the function return NULL? |
memcpy | Copy an exact number of bytes | Does the destination have enough space? |
snprintf | Format into a bounded destination | Was the result truncated? |
Some classic functions such as strcpy and strcat do not know the destination capacity. Prefer designs where you calculate and check the required space.
#include <stdio.h>
#include <string.h>
int main(void) {
char full[40] = "Master";
const char suffix[] = " Coding";
size_t used = strlen(full);
size_t available = sizeof full - used;
if (strlen(suffix) + 1 <= available) {
memcpy(full + used, suffix, strlen(suffix) + 1);
}
printf("%s (%zu characters)\n", full, strlen(full));
return 0;
}Compare & search
Arrays cannot be compared by content with ==. Use strcmp; its result is negative, zero, or positive. Search functions return a pointer to the match orNULL.
#include <stdio.h>
#include <string.h>
int main(void) {
const char left[] = "apple";
const char right[] = "apricot";
int order = strcmp(left, right);
if (order < 0) printf("%s comes first\n", left);
const char *found = strstr("learn C safely", "C");
if (found != NULL) printf("Found: %s\n", found);
return 0;
}Tokenize & parse
Numeric conversion functions such as strtol provide more control thanatoi: they report where parsing stopped and can signal range errors througherrno.
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char text[] = "42";
char *end = NULL;
errno = 0;
long value = strtol(text, &end, 10);
if (errno != 0 || end == text || *end != '\0' ||
value < INT_MIN || value > INT_MAX) {
fprintf(stderr, "Invalid integer\n");
return 1;
}
printf("Parsed: %ld\n", value);
return 0;
}strtok modifies its input
strtok can split a writable string by replacing delimiters with terminators, but it keeps hidden state. For reusable or concurrent code, prefer explicit parsing or a reentrant platform alternative.Capacity & safety
#include <stdio.h>
#include <string.h>
int main(void) {
char destination[8];
const char *source = "C language";
int needed = snprintf(destination, sizeof destination, "%s", source);
if (needed < 0) return 1;
if ((size_t) needed >= sizeof destination) {
printf("Truncated safely: %s\n", destination);
}
return 0;
}- Track destination capacity alongside every writable buffer
- Reserve one byte for the null terminator
- Check library return values and truncation
- Do not treat arbitrary binary data as a string
- Prefer length-aware interfaces for reusable functions
Recap & quick check
Key takeaways
- A C string is a character sequence terminated by a zero byte.
- String length and buffer capacity are different; capacity must include the terminator.
- Writable arrays may be modified, while string literals must not be.
- Compare contents with strcmp, not ==.
- Bound input and output, check return values, and handle truncation explicitly.
Quick check
1. How many bytes are required to store "cat" as a C string?
2. What does strcmp return when strings are equal?
3. Why prefer strtol over atoi for validated input?
4. What must a writable-string API know?
You can now handle text deliberately. Next up: Module 9 — Pointers & Addresses, the idea that connects arrays, functions, and memory.