c-foundations Lesson 12 20 min read

How Do Arrays Store and Organize Multiple Values?

Fixed-size, no bounds checking, and decay to pointers — C arrays are not Java arrays

Reading: C Text: Ch. 7 §1–2 (pp. 399–420), §5 (pp. 450–460)

After this lesson, you will be able to:

  • Declare and initialize C arrays, including zero-filling with = {0}
  • Explain why C arrays have no bounds checking and the security risk of buffer overflows
  • Pass an array and its size as separate parameters to a function
  • Explain that arrays are passed by address, so functions can modify the original
  • Use sizeof(arr) / sizeof(arr[0]) to compute element count and explain why it fails inside functions

No Safety Net

In Java, accessing arr[100] on a size-10 array throws ArrayIndexOutOfBoundsException. The program stops, you see a stack trace, you fix it.

In C, accessing arr[100] on a size-10 array does… something. Maybe it reads garbage. Maybe it overwrites another variable. Maybe it crashes. Maybe it works perfectly today and crashes tomorrow. C doesn’t check array bounds. Ever.

This is the single most dangerous difference between Java and C arrays. Let’s understand it — and learn how to stay safe.


C Arrays from Declaration to Functions

Declaring Arrays

int scores[5];                      // 5 ints, UNINITIALIZED (garbage!)
int grades[5] = {90, 85, 77, 92, 88};  // Initialized
int zeros[100] = {0};               // First element 0, rest auto-filled with 0
double temperatures[24];            // 24 doubles, uninitialized

Common Pitfall: An uninitialized C array contains whatever garbage data happened to be in that memory. Always initialize your arrays — either with values or with = {0} to zero-fill.

Accessing Elements

int grades[5] = {90, 85, 77, 92, 88};

printf("First: %d\n", grades[0]);    // 90
printf("Last: %d\n", grades[4]);     // 88

grades[2] = 80;                       // Modify the third element

Zero-indexed, just like Java.

No Bounds Checking

int arr[5] = {10, 20, 30, 40, 50};
printf("%d\n", arr[10]);    // NO ERROR! Reads garbage from memory
arr[10] = 999;              // NO ERROR! Overwrites random memory

Key Insight: C performs zero bounds checking on array access. Reading or writing past the end of an array is undefined behavior — it might work, might crash, or might corrupt other variables. This is called a buffer overflow, and it’s one of the most dangerous bugs in computing. It’s also one of the most common security vulnerabilities.

Check Your Understanding
You declare int arr[5]; without initializing it. What values do the elements contain?
A Unpredictable garbage — whatever was previously in that memory
B All zeros — C initializes arrays automatically
C All -1 to indicate "empty"
D The compiler refuses to compile — you must initialize arrays
Answer: A. Unlike Java (which initializes int[] elements to 0), C does not initialize local arrays. The elements contain whatever happened to be in that memory previously. Reading uninitialized values is undefined behavior. Use = {0} to zero-fill, or explicitly set each element before reading it.

No .length Property

Unlike Java, C arrays don’t know their own size:

int grades[5] = {90, 85, 77, 92, 88};
// grades.length ?  — NO SUCH THING IN C

You must track the size yourself:

int size = 5;
for (int i = 0; i < size; i++)
{
    printf("%d\n", grades[i]);
}

The Trick: To get the number of elements in an array declared in the current scope: sizeof(arr) / sizeof(arr[0]). This divides the total bytes by the size of one element. But this only works where the array was declared — not inside functions that receive the array as a parameter.

Arrays and Functions

Here’s a critical difference from Java: when you pass an array to a function, you must also pass its size:

void print_array(const int arr[], int size)
{
    for (int i = 0; i < size; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

Key Insight: When you pass an array to a function, the function receives a pointer to the original array, not a copy. Modifying array elements inside the function DOES affect the caller’s array. This is unlike pass-by-value for regular variables — and it’s a preview of how pointers work.

From Java: In Java, arrays are objects with a .length field and bounds checking. In C, arrays are just contiguous blocks of memory. No length, no checking, no exceptions. You pass the size as a separate parameter, and you’re responsible for staying within bounds. This is more work, but it’s exactly how memory works at the hardware level.

Check Your Understanding
You pass an array to a function and modify an element inside that function. What happens to the original array?
A The original is unchanged — arrays are passed by value like everything else in C
B The original is modified — the function received the array's memory address, not a copy
C It depends on whether you used const in the parameter
D The compiler makes a copy only if the array is small enough to fit on the stack
Answer: B. When you pass an array to a function, it "decays" to a pointer — the function gets the memory address of the first element. Any modifications go straight to the original data. This is different from passing an int or double (which are copied). The const keyword (option C) tells the compiler to prevent modification, but it doesn't change how the array is passed — it's a compile-time safety check.
Why does this matter?

This array-passing behavior is your first taste of pointer semantics, which is the central concept of Series 3. Understanding that arrays are passed by address — not by value — explains why sorting functions can rearrange your data in place, and why you need const to protect arrays you don’t want modified.

Building the Grade Tracker

#include <stdio.h>

#define MAX_GRADES 100

void read_grades(int grades[], int *count);
int find_max(const int grades[], int size);
double calculate_average(const int grades[], int size);

int main(void)
{
    int grades[MAX_GRADES];
    int count = 0;

    read_grades(grades, &count);

    printf("Max: %d\n", find_max(grades, count));
    printf("Average: %.1f\n", calculate_average(grades, count));

    return 0;
}

int find_max(const int grades[], int size)
{
    int max = grades[0];
    for (int i = 1; i < size; i++)
    {
        if (grades[i] > max)
        {
            max = grades[i];
        }
    }
    return max;
}

double calculate_average(const int grades[], int size)
{
    int sum = 0;
    for (int i = 0; i < size; i++)
    {
        sum += grades[i];
    }
    return (double)sum / size;
}

Notice const int grades[] — the const keyword tells the compiler (and the reader) that this function won’t modify the array. Use it whenever a function only reads the array.

Check Your Understanding
Inside a function that receives int arr[] as a parameter, what does sizeof(arr) return?
A The total number of bytes in the original array
B The number of elements in the array
C The size of one element (e.g., 4 bytes for int)
D The size of a pointer (typically 8 bytes on 64-bit systems)
Answer: D. When an array is passed to a function, it decays to a pointer. So sizeof(arr) gives you the size of a pointer, not the array. That's 8 bytes on a 64-bit system regardless of how many elements the array has. This is exactly why you must pass the array size as a separate parameter — the function has no way to figure it out from the pointer alone.
Quick Check: What happens if you access arr[50] on an array of size 10?

Undefined behavior. C doesn’t check array bounds. You might read garbage data, overwrite another variable, or crash. The program might even appear to work correctly — until it doesn’t. There’s no ArrayIndexOutOfBoundsException in C.

Quick Check: Why must you pass the array size as a separate parameter?

C arrays don’t carry size information. When passed to a function, the array “decays” to a pointer — the function only receives the memory address of the first element. sizeof(arr) inside the function returns the pointer size (8 bytes), not the array size. So you must pass the size explicitly.

Quick Check: Does modifying an array inside a function affect the original?

Yes! Unlike regular variables (which are passed by value), arrays are effectively passed by pointer. The function receives the memory address of the original array, so changes to elements are visible to the caller.


Power Without Protection

C arrays give you direct access to contiguous memory — fast and flexible. But the lack of bounds checking means every out-of-bounds access is a potential crash, security vulnerability, or data corruption bug. The discipline of always tracking array sizes and checking indices is your responsibility.

Next: strings as character arrays. In C, there’s no String class — strings are just arrays of char terminated by a null byte. This is the biggest conceptual shift from Java, and understanding it is essential for every lab from here on.

Big Picture: Buffer overflows — writing past the end of an array — are the #1 cause of security vulnerabilities in C programs. The Morris Worm (1988), Heartbleed (2014), and countless other exploits all used buffer overflows. Understanding array bounds isn’t just about avoiding bugs; it’s about writing secure software.