Arrays and Strings
|
This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
An array in C is a contiguous block of objects of one type, and a string is nothing more than a char array
whose end is marked by a '\0' byte. There is no array length stored anywhere, no bounds checking, and no
string type — which is the source of both C’s speed and most of its security history.
Declaring Arrays and Knowing Their Length
#include <stddef.h>
#include <stdio.h>
int main(void)
{
int fixed[5]; // 5 ints, indeterminate values
int zeroed[5] = { 0 }; // all five are zero
int listed[5] = { 1, 2, 3, 4, 5 };
int inferred[] = { 1, 2, 3 }; // size 3, from the initializer
int sparse[10] = { [9] = 1 }; // designated: element 9 is 1, rest zero
// The idiomatic element count -- and the only correct one:
size_t count = sizeof listed / sizeof listed[0];
fixed[0] = 1;
printf("%zu %d %d %d %d\n", count, zeroed[4], listed[4], inferred[2], sparse[9]);
return 0;
}
Indices run 0 to n-1. Reading or writing array[n] — or any index out of range — is undefined
behavior, not an error: no exception, no bounds check, just a corrupted neighbour or a crash later. That is
what -fsanitize=address is for.
The sizeof array / sizeof array[0] idiom is worth wrapping, with a caveat:
#include <stddef.h>
#include <stdio.h>
#define ARRAY_COUNT(a) (sizeof (a) / sizeof (a)[0])
// Once an array is passed, its length must travel with it as a parameter.
static int sum(const int *values, size_t n)
{
int total = 0;
for (size_t i = 0; i < n; ++i) {
total += values[i];
}
return total;
}
int main(void)
{
int values[10] = { 1, 2, 3 };
printf("%zu %d\n", ARRAY_COUNT(values), sum(values, ARRAY_COUNT(values))); // 10 6
return 0;
}
ARRAY_COUNT is only valid where the array declaration itself is visible. Inside a function declared as
void f(int values[10]), values is a pointer, so the same macro computes
sizeof(int *) / sizeof(int) — 2 on LP64. Clang and GCC both catch this one:
warning: 'sizeof (values)' will return the size of the pointer,
not the array itself [-Wsizeof-pointer-div]
warning: sizeof on array function parameter will return size of 'int *'
instead of 'int[10]' [-Wsizeof-array-argument]
So once an array has decayed to a pointer, its length must travel as a separate argument — which is exactly
what every <string.h> and <stdlib.h> function that takes an n is doing.
Multidimensional Arrays
C has no "2-D array" type — it has arrays of arrays, stored in row-major order with no gaps:
#include <stdio.h>
int main(void)
{
int grid[2][3] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
};
// Memory layout: 1 2 3 4 5 6 -- rows back to back.
const int *flat = &grid[0][0];
printf("%d %d %d\n", grid[1][2], flat[5], *(*(grid + 1) + 2)); // all 6
printf("sizeof grid = %zu, sizeof grid[0] = %zu, rows = %zu\n",
sizeof grid, sizeof grid[0], sizeof grid / sizeof grid[0]);
return 0;
}
grid[i][j] is ((grid + i) + j): the first index selects a row (itself an array of 3 int), the second
an element. That is why only the first dimension may be omitted in a parameter — the compiler needs the row
width to do the arithmetic:
#include <stddef.h>
#include <stdio.h>
// The row width is part of the type; only the leading dimension may be left out.
static int sum_fixed(int matrix[][3], size_t rows)
{
int total = 0;
for (size_t r = 0; r < rows; ++r) {
for (size_t c = 0; c < 3; ++c) {
total += matrix[r][c];
}
}
return total;
}
// C99 variably-modified parameter: pass both dimensions and index naturally.
static int sum_any(size_t rows, size_t cols, int matrix[rows][cols])
{
int total = 0;
for (size_t r = 0; r < rows; ++r) {
for (size_t c = 0; c < cols; ++c) {
total += matrix[r][c];
}
}
return total;
}
int main(void)
{
int grid[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
printf("%d %d\n", sum_fixed(grid, 2), sum_any(2, 3, grid));
return 0;
}
Variable-Length Arrays
A VLA’s length is a run-time expression. C99 made them mandatory, C11 optional
(__STDC_NO_VLA__ says an implementation lacks them), and C23 keeps them optional while
requiring variably-modified pointer types like the int (*)[cols] above.
#include <stdio.h>
static double mean(size_t n, const double values[n]) // a VLA parameter -- fine everywhere
{
double total = 0.0;
for (size_t i = 0; i < n; ++i) {
total += values[i];
}
return n ? total / (double)n : 0.0;
}
int main(void)
{
size_t n = 4;
double samples[4] = { 1.0, 2.0, 3.0, 4.0 };
// A local VLA: allocated on the stack, so the size MUST be small and trusted.
double scratch[n];
for (size_t i = 0; i < n; ++i) {
scratch[i] = samples[i] * 2.0;
}
printf("%g %g\n", mean(n, samples), mean(n, scratch));
return 0;
}
Use VLA parameters freely — they are just documentation plus better indexing. Be wary of local VLAs: a
length that comes from input is a stack overflow waiting to happen (-Wvla and MISRA both ban them), and
malloc gives you a failure you can check. See
Dynamic Memory Allocation.
Array-to-Pointer Decay
In almost every expression an array decays to a pointer to its first element. The exceptions are sizeof,
alignof, &, and a string literal initializing a char array:
#include <stdio.h>
int main(void)
{
int values[4] = { 1, 2, 3, 4 };
int *p = values; // decay: same as &values[0]
int (*whole)[4] = &values; // pointer to the ARRAY, a different type
printf("%zu %zu\n", sizeof values, sizeof p); // 16 8 -- decay loses the size
printf("%d %d %d\n", *p, p[2], (*whole)[3]);
printf("%d\n", *(values + 1)); // values[1]: indexing IS pointer arithmetic
printf("%d\n", 1[values]); // legal, and a good argument for never doing it
return 0;
}
C99’s [static n] in a parameter turns "I expect at least n elements" into something the compiler can check
and optimize with:
#include <stddef.h>
// "values points to at least 1 int" -- passing nullptr is now diagnosable
// (-Wnonnull), and the compiler may assume the dereference is safe.
static int first(size_t n, const int values[static 1])
{
(void)n;
return values[0];
}
int main(void)
{
int data[3] = { 7, 8, 9 };
return first(3, data) - 7;
}
Strings Are char Arrays
There is no string type. A "string" is a char array containing a '\0'; every library function finds the end
by scanning for that byte:
#include <stdio.h>
#include <string.h>
int main(void)
{
char writable[] = "hello"; // an array of 6 chars: 'h','e','l','l','o','\0'
const char *literal = "hello"; // a pointer to a NON-modifiable string literal
writable[0] = 'H'; // fine -- writable is our own array
// literal[0] = 'H'; // UNDEFINED BEHAVIOR: literals may be read-only
printf("%s %s %zu %zu %zu\n",
writable, literal,
strlen(writable), // 5 -- characters before the NUL
sizeof writable, // 6 -- the array, including the NUL
sizeof literal); // 8 -- the pointer, on LP64
return 0;
}
The two lines to internalize: strlen is O(n) (never call it in a loop condition over the same string), and
sizeof on a char array includes the NUL while strlen does not. Off-by-one between those two is the
canonical C buffer overflow.
String Literals
#include <stdio.h>
int main(void)
{
const char *joined = "adjacent " "literals " "are concatenated";
const char *escapes = "tab:\t newline:\n quote:\" backslash:\\ nul-in-middle:\0hidden";
const char *long_line = "a very long message that "
"continues on the next source line";
printf("%s\n%s\n%s\n", joined, escapes, long_line);
printf("%zu\n", sizeof "abc"); // 4
return 0;
}
A string literal has type char[N] (not const char[N], for historical reasons) but writing to it is
undefined — so always point at one with const char *. -Wwrite-strings makes the compiler enforce it.
char Arrays vs. Pointers
#include <stdio.h>
#include <string.h>
int main(void)
{
char buffer[16] = "start"; // 16 bytes of our own storage, copied into
const char *view = "start"; // 8 bytes pointing at shared, read-only storage
strcpy(buffer, "changed"); // fits: 8 bytes including NUL
// strcpy(buffer, "this string is far too long for the buffer"); // overflow -- UB
printf("%s %s %zu\n", buffer, view, strlen(buffer));
return 0;
}
Declare char buffer[N] when you need to modify or build a string, and const char * when you only need to
read one.
The <string.h> Essentials
| Function | Does | Watch out for |
|---|---|---|
|
Length before the NUL |
O(n); undefined if |
|
Copy / append, including the NUL |
No bounds check at all — the caller guarantees the space. |
|
Copy at most |
Does not NUL-terminate if |
|
Formatted write, always NUL-terminates |
The one to use. Returns the length it wanted, so a return |
|
Lexicographic compare |
Returns |
|
Find a character / substring |
Return a pointer into the string, or |
|
Span and tokenize |
|
|
Copy |
|
|
Fill / compare bytes |
|
|
Allocate a copy (C23; POSIX before that) |
The caller must |
The safe-formatting pattern that replaces strcpy/strcat entirely:
#include <stdio.h>
#include <string.h>
int main(void)
{
char path[32];
const char *dir = "/var/log";
const char *file = "app.log";
int written = snprintf(path, sizeof path, "%s/%s", dir, file);
if (written < 0) {
return 1; // encoding error
}
if ((size_t)written >= sizeof path) {
fprintf(stderr, "path truncated (needed %d bytes)\n", written + 1);
return 1; // handle it -- do not use the result
}
printf("%s (%zu chars)\n", path, strlen(path));
return 0;
}
C23 adds memset_explicit (a memset the optimizer may not remove, for wiping secrets) and standardizes
strdup/strndup. The optional Annex K _s functions (strcpy_s, …) exist but are implemented almost
nowhere outside MSVC — see
Standard Library Overview.
See Also
-
Pointers — pointer arithmetic, which is what indexing is.
-
Strings and Text Processing — the full
<string.h>/<ctype.h>tour, numeric conversion, UTF-8 and locales. -
Dynamic Memory Allocation — growable arrays and strings.
-
Structures, Unions and Type Aliases — flexible array members.
-
C++: Containers — C++ replaces most raw-array work with
std::vector,std::arrayandstd::span.
References
-
WG14 N3220 — the C23 working draft (§6.7.7.3 "Array declarators", §6.3.2.1 "Lvalues, arrays, and function designators", §7.26 "String handling").
-
GNU C Library Reference Manual — String and Array Utilities.