---
title: 'Arrays and Strings'
source: 'https://academia.sh/en/courses/c-fundamentals/arrays-and-strings'
course: 'C Fundamentals'
language: en
updated: '2026-09-26T03:02:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes, by measuring, that array length is determined at compile time and string length at runtime'
    - "Shows with an example how the terminating byte determines a string's boundary at runtime"
    - 'Measures the array–pointer relationship through element value and difference, without printing an address'
---

# Arrays and Strings

An array's element count being determined at compile time through constant folding, and a string's length being determined at runtime by scanning until the terminating byte; the array–pointer relationship measured through element value and difference without printing an address.

The Functions lesson measured which definition a call is bound to, how an argument is
copied, and how a return is interpreted. This lesson returns to the transferred data
itself, comparing how two different data kinds carry their length: an **array's** length
and a **string's** length, even though both rest on the same memory layout, are decided
at two separate stages.

The Programming Fundamentals course (M01/K02) already established the list and string
concepts with Python; there, a list's length was information that came along on its own,
always askable with a call (`len`). C's counterpart is measured here: an array's length
is produced at compile time from a constant expression written in the source, while a
string's length is stored nowhere at all — it is learned only by scanning at runtime
until a terminating byte is reached. This lesson is the one that shows both ends of the
course's measurement axis at once: two neighboring values sitting in the same memory
layout give the answer "the source is enough" for one, and "only runtime is enough" for
the other.

## Constant Folding: Array Length at Compile Time

The excerpt below is part of the core introduced in the Compilation Stages lesson: an
array's size is produced from a constant expression written in the source.

```c
// stage.c — constant folding (compiler) and string length (runtime) excerpt
#include <stdio.h>
#include <string.h>

int main(void) {
    int constant_array[2 * 3 + 1];
    printf("%s %d\n", "constant folding", (int)(sizeof constant_array / sizeof constant_array[0]));

    const char *text = "hello, world!";
    printf("%s %d\n", "string length", (int)strlen(text));
    return 0;
}
```

```
constant folding 7
string length 13
```

**BS9.** The expression `2 * 3 + 1` is bound to no variable — it is built only from
constants, and the compiler **folds** this expression down to `7` without the program
ever running; `constant_array`'s element count is already written into the binary as `7`.
`strlen(text)` is the exact opposite: the byte sequence the `text` pointer points to is
scanned one byte at a time until `\0` is found, and the length (`13`) only emerges once
this scan is complete. Both measurements sit on the same line, in the same function, but
one confirms a number that could be read from the source, while the other produces a
number that can only be learned by running it.

Because `constant_array`'s size is embedded in the binary, the expression `sizeof
constant_array` itself requires no computation at runtime either — the compiler has
already replaced this expression with a constant. This shows that `sizeof`, in most of
its uses, is a **compile-time** operator: it produces a result by looking only at its
operand's type, never at its actual value. In a declaration like `constant_array[2 * 3 +
1]`, the expression inside the brackets falls into the same category — the expression
determining an array's size must be a **constant expression**; had a variable or a
function call entered it, the compiler could no longer know the size in advance, and this
would enter C's separate subject of variable-length arrays. The array measured in this
lesson always has a fixed length.

## The Terminating Byte's Role: String Length at Runtime

A string's length is stored nowhere separately; `strlen` and similar functions
**compute** the length by scanning memory until they encounter a `\0` byte. When the
terminating byte's position changes, the same memory area reports a different length.

```c
// terminator.c — the terminating byte's position determines at runtime where a string ends
#include <stdio.h>
#include <string.h>

int main(void) {
    char short_string[6] = {'h', 'e', 'l', 'l', 'o', '\0'};
    printf("%s %d\n", "length while the terminator is at the end", (int)strlen(short_string));

    short_string[2] = '\0';
    printf("%s %d\n", "length once the terminator is placed early", (int)strlen(short_string));
    return 0;
}
```

```
length while the terminator is at the end 5
length once the terminator is placed early 2
```

**BS10.** The `short_string` array allocates six bytes, the first five letters and the
sixth `\0`; `strlen` returns `5` because it encounters the terminator right after the
fifth byte. Once `short_string[2]` is changed to `'\0'`, the array itself is still six
bytes — no memory was reallocated, no element was removed — but `strlen` now returns `2`,
because the scan stops the instant it reaches the third byte. This shows that a string's
"length" is not actually a storage property but a **runtime result**: the same memory
layout can report a different length depending on the terminator's position, and that
position is an ordinary byte that can change at runtime.

This also explains why a string is not a type of its own in C: `char short_string[6]` is
a `char` array, and to the language's eyes carries nothing more than six separate `char`
values. "String" is only an interpretation built on a **convention** — that a `\0` byte
will be found somewhere inside it, and that byte marks the boundary — not a property the
language itself enforces. There is no such convention for an integer array (`int
array[]`) — there is no part of the language stating a contract for which element marks
the "end"; this is why an integer array's length is either determined by a constant
expression in the source (like `constant_array` above) or carried separately in a
variable. A string is a special case, only for `char` arrays, that carries its own length
within itself through the terminating-byte convention. Manually writing `'\0'` into
`short_string[2]` does not disturb the array's own memory layout at all; it only changes
where this convention stops. Functions like `strlen` **assume** this convention — they
trust that a `\0` really exists somewhere in the memory a pointer passed to them points
to; if the terminator is absent entirely, the scan runs past the array's allocated
boundary, and this is an undefined-behavior scenario this course's general rule does not
try.

## The Array–Pointer Relationship: Measuring Without Printing an Address

In most contexts, an array's name **decays** into a pointer to its first element. This
relationship can be measured without printing an address, purely through element value
and difference.

```c
// relationship.c — the array–pointer relationship measured through value and difference, no address printed
#include <stdio.h>

int main(void) {
    int array[] = {10, 20, 30, 40};
    int *p = array;

    printf("%s %d\n", "value accessed via p's offset", *(p + 2));
    printf("%s %d\n", "value accessed via array indexing", array[2]);
    printf("%s %d\n", "are the two accesses the same", (int)(*(p + 2) == array[2]));
    printf("%s %d\n", "are the addresses the same", (int)((p + 2) == &array[2]));
    printf("%s %d\n", "difference in elements", (int)(&array[3] - &array[0]));
    return 0;
}
```

```
value accessed via p's offset 30
value accessed via array indexing 30
are the two accesses the same 1
are the addresses the same 1
difference in elements 3
```

**BS11.** With the assignment `p = array`, `p` takes the address of the array's first
element; `*(p + 2)` and `array[2]` produce the same value (`30`), because `array[i]` is
already shorthand for `*(array + i)` — both compute the same address and read the same
value. The comparison `(p + 2) == &array[2]` confirms this too: the two addresses are
equal. The difference `&array[3] - &array[0]` is not a raw byte difference but a
difference **in elements** — the result is `3`, because there are three `int`-widths of
stride between the two pointers; this is a behavior the difference operator has specially
defined for pointers in C, given already divided by the `int` width. No step printed a
real address; everything measured was a value, an equality, or an element difference.

That `array[2]` is only shorthand for `*(array + 2)` shows that C does not dedicate a
separate mechanism to array indexing: the square bracket is a syntactic convenience built
on top of pointer arithmetic. This is also why the notations `array[2]` and `2[array]`
mean the same thing — because addition is commutative, `array + 2` and `2 + array` give
the same address, and both notations go through the same translation. This lesson does
not measure that equivalence, but it does confirm that what underlies `array[i]` is
genuinely an addition and a **dereference** — not some special "array access"
instruction. This is a language-level equivalence: both notations compute the same
address and give the same value. How many steps the compiler's produced machine code
spends is a separate question — `array` is an array name, `p` is a variable that must be
read from memory — and this lesson does not measure that.

This equivalence leaves a fine distinction between the array itself and the pointer the
array decays into: the array name by itself carries the name of the whole array, while
`p` is a pointer variable and can be reassigned to point to another array — `p =
other_array;` is valid, but `array = other_array;` is not, because `array` is an array
declaration, not a reassignable pointer variable. As a rule, an array name decays into
the address of its first element whenever it occurs in an expression; a few cases are
exceptions to this, such as being the operand of `sizeof` or `&`. What decays is an
address value, not a reassignable pointer variable.

## The Bounding Measurement

An array's length is known at compile time, but this information does not pass to a
function. A function taking an array as a parameter receives not the array itself but
only the address of its first element, and cannot know the length on its own.

```c
// length.c — array length information does not pass to a call, the receiving side must be given it separately
#include <stdio.h>
#include <stddef.h>

static int sum_assuming_four(int data[]) {
    /* this function does not know the array's real size: it assumes a count of 4 */
    int sum = 0;
    for (int i = 0; i < 4; i++) {
        sum += data[i];
    }
    return sum;
}

static int sum_with_length(int data[], size_t length) {
    int sum = 0;
    for (size_t i = 0; i < length; i++) {
        sum += data[i];
    }
    return sum;
}

int main(void) {
    int large[] = {1, 2, 3, 4, 5, 6};
    printf("%s %d\n", "sum with an assumed fixed count", sum_assuming_four(large));
    printf("%s %d\n", "sum with a given length", sum_with_length(large, 6));

    int small_array[] = {10, 20, 30};
    printf("%s %d\n", "sum with a given length, different size", sum_with_length(small_array, 3));
    return 0;
}
```

```
sum with an assumed fixed count 10
sum with a given length 21
sum with a given length, different size 60
```

Even though `int data[]` is written in `sum_assuming_four`'s source, it only receives an
`int *` when called — it carries no information at all about the array's real size. The
function has written its loop with a fixed bound of `4` and sums `large`'s first four
elements, giving `10` — short of the array's real sum, `21`, because the last two
elements are never seen. `sum_with_length`, by contrast, takes its length as a separate
parameter (`length`) and produces the correct sum (`21`); the same function, called with
a differently sized array like `small_array`, also gives the correct result (`60`),
because it takes its loop bound from the caller instead of writing it as a fixed value in
the source. This shows how the "a parameter is always a copy" rule measured in the
Functions lesson works for an array: what gets copied is not the array itself, but the
address of its first element — and alongside that address, the array's element count is
lost unless it is carried separately.

The `int data[]` written in `sum_assuming_four`'s declaration is interpreted by the
compiler as `int *data` — an array declaration in a parameter list is always converted to
a pointer declaration in C. This is why writing `sizeof(data)` inside the function body
would not give the array's size but a pointer's size; since no size value is printed in
this course, this lesson does not measure that error, but it does explain why
`sum_assuming_four` **had** to assume a fixed number (`4`): the function can in no way
derive the array's real length from the pointer it is given. This is a direct
consequence of the array–pointer relationship measured above: when an array passes to a
function, only the address of the first element remains, no metadata belonging to the
array itself (such as element count) travels along with that address. This is also the
same constraint behind why the standard library's string functions (`strlen` included)
encode length information with a terminating byte: if only a starting address can be
passed to a function, where the data ends must be carried along with that data, as a
mark.

## Summary

- An array's element count is produced at **compile time** from a constant expression in
  the source (constant folding); class **compiler**.
- A string's length is stored nowhere; it is computed at **runtime** by scanning until
  the terminating `\0` byte is reached; class **runtime**. This lesson thus carries two
  classes at once.
- The terminating byte's position can change at runtime; the same memory area can report
  a different string length depending on the terminator's placement.
- The array–pointer relationship can be measured without printing an address: element
  value (`*(p + i)` and `array[i]` are the same), equality, and difference in elements
  are enough.
- An array's length information does not pass to a function; the receiving side cannot
  know the array's real size on its own unless it takes the length as a separate
  parameter.
- An array declared as a parameter is converted by the compiler to a pointer; the
  string's terminating-byte convention exists precisely for this reason — so that
  functions forced to work with only a starting address can carry the length along with
  the data.

## Next Step

The two data kinds measured in this lesson — array and string — always stayed within the
boundary of a single variable. The Structs, Unions and Enumerations lesson measures three
new mechanisms that gather several values under a single name: how a struct's fields are
laid out, how a union shares the same memory, and how an enumeration constant turns into
a compile-time constant. This lesson measured that an array's length comes from a
constant expression in the source; in the next lesson, the same constant-expression idea
will explain how an enumeration value is produced. Arrays and strings here always carried
a single-type sequence of values — all `int` or all `char`; structs, by contrast, will
gather values of different types under a single name, and how those values are laid out
in memory will again be a question decided at compile time.
