05 · Arrays & Strings¶
Arrays hold a fixed-size, contiguous block of same-typed values. C strings are
just arrays of char with a special end-of-string marker — there is no
built-in string type.
Declaring and using arrays¶
#include <stdio.h>
int main(void) {
int scores[5] = {90, 85, 77, 92, 68};
printf("%d\n", scores[0]); // 90 -- indexing starts at 0
printf("%d\n", scores[4]); // 68 -- last valid index is size - 1
scores[2] = 80; // arrays are mutable
printf("%d\n", scores[2]); // 80
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += scores[i];
}
printf("sum = %d\n", sum); // sum = 415
return 0;
}
C does not check array bounds for you. Reading or writing scores[5] or
scores[-1] compiles and often "works" until it silently corrupts nearby
memory or crashes — always track the size yourself (e.g. with a #define or
a separate size_t count variable) and stay inside it.
Strings are char arrays¶
A C string is a char array ending in a null terminator, '\0':
#include <stdio.h>
#include <string.h>
int main(void) {
char name[20] = "Ada"; // compiler adds the '\0' automatically
printf("%s\n", name); // Ada
printf("%zu\n", strlen(name)); // 3 -- length NOT counting '\0'
printf("%zu\n", sizeof(name)); // 20 -- full buffer size
return 0;
}
sizeof and strlen answer different questions: sizeof is the buffer's
total capacity; strlen is how many characters are actually in use before the
terminator.
Common <string.h> functions¶
#include <stdio.h>
#include <string.h>
int main(void) {
char greeting[50] = "Hello";
strcat(greeting, ", world!"); // append -- buffer must be big enough
printf("%s\n", greeting); // Hello, world!
char copy[50];
strcpy(copy, greeting); // copy into another buffer
printf("%s\n", copy); // Hello, world!
if (strcmp("abc", "abc") == 0) { // 0 means equal
printf("equal\n");
}
printf("%zu\n", strlen(greeting)); // 13
return 0;
}
strcat and strcpy do not check that the destination buffer is large
enough — writing past the end silently corrupts memory (a classic source of
real-world security bugs). Prefer the bounded versions strncat/strncpy
when the input size isn't fully controlled, and always size destination
buffers generously.
Reading strings from input safely¶
#include <stdio.h>
int main(void) {
char name[50];
printf("Enter your name: ");
fgets(name, sizeof(name), stdin); // safe: won't overflow the buffer
// fgets keeps the trailing newline -- strip it if present
size_t len = strlen(name);
if (len > 0 && name[len - 1] == '\n') {
name[len - 1] = '\0';
}
printf("Hello, %s!\n", name);
return 0;
}
Avoid gets() entirely — it has no way to limit how much it reads and is
banned from modern C for that reason. fgets with an explicit buffer size is
the safe replacement.
Multi-dimensional arrays¶
#include <stdio.h>
int main(void) {
int grid[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
for (int row = 0; row < 2; row++) {
for (int col = 0; col < 3; col++) {
printf("%d ", grid[row][col]);
}
printf("\n");
}
// Output:
// 1 2 3
// 4 5 6
return 0;
}
Cheat sheet¶
| Task | Function/Syntax |
|---|---|
| Declare array | int arr[5]; |
| Declare + initialize | int arr[5] = {1,2,3,4,5}; |
| Array length (elements) | track it yourself, or sizeof(arr)/sizeof(arr[0]) for stack arrays |
String length (chars, no \0) |
strlen(s) |
| Copy string | strcpy(dst, src) / strncpy (bounded) |
| Append string | strcat(dst, src) / strncat (bounded) |
| Compare strings | strcmp(a, b) == 0 means equal |
| Read a line safely | fgets(buf, sizeof(buf), stdin) |
How It Actually Works¶
An array declaration like int scores[5] reserves one contiguous run of
memory — 5 × sizeof(int) = 20 bytes on a typical platform — and the array
name is really just a compile-time constant: the address of that block's
first byte. scores[i] is not a special indexing mechanism; the compiler
translates it into pointer arithmetic: *(scores + i), which computes
base_address + i * sizeof(int) and dereferences that address. This is
exactly why C performs no bounds checking — scores[5] computes a
perfectly valid-looking address (5 slots past the base), and the CPU happily
reads or writes whatever byte pattern happens to live there, whether it
belongs to another variable, the stack's saved return address, or unmapped
memory that triggers a segmentation fault. There is no array object at
runtime tracking its own length; that information exists only in the source
code and the compiler's symbol table, discarded once compilation is done.
A string is this same flat-array model plus one convention: a '\0'
byte (all-zero bits) marking where the meaningful data ends. strlen
doesn't "ask the string its length" — it walks memory byte by byte starting
at the given address, incrementing a counter, until it hits a zero byte.
That's an O(n) scan every single call, which is why calling strlen inside
a loop condition repeatedly rescans from the start each iteration — a real
performance trap in larger programs. sizeof(name) on an array, by
contrast, costs nothing at runtime: the compiler already knows the array's
declared size and substitutes the constant directly, unrelated to where any
'\0' happens to sit.
This also explains why strcpy/strcat overflow silently: they keep
copying/writing bytes from the source until they encounter its '\0', with
zero awareness of how large the destination buffer actually is. If the
source is longer than the destination's declared size, the write simply
continues past the buffer's last byte into whatever memory sits next —
adjacent stack variables, or (in the historically famous case of gets) the
saved return address itself, which is the classic mechanism behind
stack-smashing buffer-overflow exploits.
A 2-D array like int grid[2][3] is still one contiguous block (24 bytes),
laid out row-major: all of row 0's elements, then all of row 1's,
back-to-back. grid[row][col] compiles to
*(base + row * 3 * sizeof(int) + col * sizeof(int)) — the compiler bakes
the row width (3) into the address arithmetic at compile time, which is why
you must specify all dimensions but the first when passing multi-dimensional
arrays to functions.
🔀 See this in another language¶
Exercise¶
Write a program that declares a char buffer, reads a line of input into it
with fgets, strips the trailing newline, and then prints: the string itself,
its length via strlen, and the string reversed (build the reversed version
character-by-character into a second buffer — don't use any library reversal
function).