Project 2: Arrays vs Pointers - Proving They Are Different
Build a comprehensive test suite that definitively demonstrates the fundamental differences between arrays and pointers in C.
Quick Reference
| Attribute | Value |
|---|---|
| Language | C |
| Difficulty | Level 2 (Beginner-Intermediate) |
| Time | Weekend (6-12 hours) |
| Book Reference | Expert C Programming, Chapters 4, 9, 10 |
| Coolness | Interview Essential - Top misconception |
| Portfolio Value | High - Shows deep understanding |
Learning Objectives
By completing this project, you will:
- Prove arrays are not pointers - Demonstrate with concrete evidence that they are different types
- Understand array decay - Know exactly when and why arrays convert to pointers
- Identify the three non-decay contexts - Master sizeof, &, and string initializers
- Avoid the extern mismatch disaster - Understand why
extern char *svschar s[]breaks - Explain lvalue vs rvalue - Know why arrays are non-modifiable lvalues
- Debug real array/pointer bugs - Recognize and fix common mistakes
- Pass multi-dimensional arrays correctly - Handle
int a[M][N]in function parameters - Distinguish stack arrays from heap pointers - Understand memory implications
The Core Question You’re Answering
“If arrays ‘decay’ to pointers in most expressions, why are they fundamentally different types, and what disasters occur when we confuse them?”
This is one of the most common misconceptions in C programming. Many developers believe “arrays and pointers are the same thing” because they can often be used interchangeably. This project will prove that belief wrong through empirical testing.
Theoretical Foundation
The Fundamental Difference
THE TRUTH ABOUT ARRAYS AND POINTERS:
====================================
Array: A contiguous block of memory containing elements
The array IS the memory itself
Pointer: A variable that holds a memory address
The pointer POINTS TO memory elsewhere
MEMORY LAYOUT COMPARISON:
=========================
int arr[5] = {10, 20, 30, 40, 50};
┌──────────────────────────────────────────────────┐
│ Address: 0x1000 │
├──────┬──────┬──────┬──────┬──────┬──────────────┤
│ 10 │ 20 │ 30 │ 40 │ 50 │ │
├──────┴──────┴──────┴──────┴──────┘ │
│ arr IS this block of memory │
│ There is no separate pointer variable │
│ sizeof(arr) = 20 bytes (5 * 4) │
└──────────────────────────────────────────────────┘
int *ptr = arr;
┌──────────────────────────────────────────────────┐
│ Address: 0x2000 │
├──────────────────┐ │
│ 0x1000 │ ptr CONTAINS an address │
├──────────────────┘ │
│ sizeof(ptr) = 8 bytes (pointer size) │
│ ptr can be reassigned to point elsewhere │
└────────────│─────────────────────────────────────┘
│
│ Points to
▼
┌──────────────────────────────────────────────────┐
│ Address: 0x1000 (could be arr, or anywhere) │
├──────┬──────┬──────┬──────┬──────┬──────────────┤
│ 10 │ 20 │ 30 │ 40 │ 50 │ │
└──────┴──────┴──────┴──────┴──────┴──────────────┘

When Arrays Decay to Pointers
Array decay is an implicit conversion that happens in most (but not all) contexts:
DECAY RULE:
===========
When an array expression is used in a value context, it is converted
to a pointer to its first element.
int arr[5];
int *p = arr; // arr decays to &arr[0]
WHAT THE COMPILER DOES:
=======================
int arr[5];
// These are equivalent due to decay:
int *p = arr; // arr → &arr[0]
int *p = &arr[0]; // explicit
// These use decay:
func(arr); // arr → &arr[0], func receives pointer
arr + 1; // arr → &arr[0], then pointer arithmetic
arr[i]; // arr → &arr[0], then *(arr + i)
The Three Contexts Where Arrays Do NOT Decay
This is crucial knowledge:
NON-DECAY CONTEXT 1: sizeof
============================
int arr[10];
sizeof(arr); // Returns 40 (10 * sizeof(int))
// NOT 8 (pointer size)
int *ptr = arr;
sizeof(ptr); // Returns 8 (pointer size)
This is HOW you can prove they are different!
NON-DECAY CONTEXT 2: Address-of operator &
==========================================
int arr[10];
&arr; // Type: int (*)[10] - pointer to array of 10 ints
// Value: address of entire array
arr; // Type: int * (after decay)
// Value: address of first element
&arr and arr have the SAME VALUE but DIFFERENT TYPES!
int (*p)[10] = &arr; // Correct
int **pp = &arr; // WRONG! Type mismatch
NON-DECAY CONTEXT 3: String literal initializers
=================================================
char s1[] = "hello"; // Creates array of 6 chars (with \0)
// s1 IS the array, lives on stack
char *s2 = "hello"; // Creates pointer
// Points to string literal in .rodata
// s2[0] = 'H'; // UNDEFINED BEHAVIOR!
The Extern Mismatch Disaster
This is a classic bug covered extensively in Expert C Programming Chapter 4:
THE DISASTER SCENARIO:
======================
// file1.c
char arr[] = "hello"; // Array definition
// arr IS the memory containing "hello\0"
// file2.c
extern char *arr; // WRONG! Declares arr as pointer
// Linker doesn't check types
// What happens:
// 1. file2.c thinks arr is a pointer (8 bytes)
// 2. It reads the first 8 bytes of "hello\0??" as an address
// 3. 'h' = 0x68, 'e' = 0x65, 'l' = 0x6C...
// 4. Computed address: something like 0x006C6C6568
// 5. Dereferencing this garbage address = CRASH or UB
MEMORY VIEW OF THE BUG:
=======================
file1.c creates:
┌─────────────────────────────────────────┐
│ arr: │ 'h' │ 'e' │ 'l' │ 'l' │ 'o' │ 0 │
│ └──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴───┤
│ bytes: 68 65 6C 6C 6F 00 │
└─────────────────────────────────────────┘
file2.c expects:
┌─────────────────────────────────────────┐
│ arr: │ 8-byte pointer │
│ │ value = ??? │
│ │ Reads: 0x006F6C6C65680000 (LE) │
└─────────────────────────────────────────┘
When file2.c does: printf("%s\n", arr);
- It thinks arr is a pointer
- Reads the bytes as an address
- Jumps to that bogus address
- SEGFAULT!
THE FIX:
========
// file1.c
char arr[] = "hello";
// file2.c
extern char arr[]; // CORRECT! Matches the definition
Arrays Are Non-Modifiable Lvalues
LVALUE VS RVALUE:
=================
An lvalue designates an object (has an address)
An rvalue is a value (temporary, no persistent address)
int x = 5; // x is an lvalue
int *p = &x; // Can take address of lvalue
int *q = &5; // ERROR: can't take address of rvalue
ARRAYS ARE SPECIAL:
===================
int arr[10]; // arr is an lvalue (designates memory)
// BUT arr is non-modifiable
arr = other; // ERROR: can't assign to array
arr++; // ERROR: can't modify array
int *ptr;
ptr = arr; // OK: ptr is modifiable lvalue
ptr++; // OK: can modify pointer
WHY?
====
Arrays would need to be "rebindable" to support assignment.
But C arrays ARE their memory - there's no separate binding.
You can't make an array suddenly refer to different memory.
This is like asking "can I make variable x occupy a different address?"
Function Parameters: Arrays Become Pointers
ARRAY PARAMETERS DECAY:
=======================
// These are IDENTICAL:
void func(int arr[10]);
void func(int arr[]);
void func(int *arr);
// The compiler treats them all as:
void func(int *arr);
PROOF:
======
void test(int arr[100]) {
printf("sizeof = %zu\n", sizeof(arr)); // Prints 8, not 400!
}
int main() {
int arr[100];
printf("sizeof = %zu\n", sizeof(arr)); // Prints 400
test(arr); // Passes pointer
}
MULTI-DIMENSIONAL ARRAYS:
=========================
// For int arr[M][N], the parameter must be:
void func(int arr[][N]); // First dimension can be empty
void func(int (*arr)[N]); // Or explicit pointer to array
// These are equivalent:
void func(int arr[10][20]);
void func(int arr[][20]);
void func(int (*arr)[20]);
// This is WRONG:
void func(int **arr); // Double pointer != 2D array!
Project Specification
What You Will Build
A comprehensive test suite called array_pointer_lab that proves arrays and pointers are different through empirical testing:
$ ./array_pointer_lab --test sizeof
=== sizeof Test ===
int arr[10]: sizeof = 40 bytes
int *ptr: sizeof = 8 bytes
PROOF: sizeof(array) != sizeof(pointer)
$ ./array_pointer_lab --test address
=== Address-of Test ===
&arr type: int (*)[10] value: 0x7fff5fbff8d0
arr type: int * value: 0x7fff5fbff8d0
PROOF: Same value, different types!
$ ./array_pointer_lab --test all
[Running all tests...]
✓ sizeof test passed
✓ address test passed
✓ decay test passed
✓ non-decay test passed
✓ extern mismatch test passed
✓ function parameter test passed
✓ string literal test passed
✓ 2D array test passed
8/8 tests passed
Functional Requirements
- sizeof Demonstration:
- Show sizeof differences between arrays and pointers
- Test with different types and sizes
- Show sizeof in function parameters
- Address-of Demonstration:
- Show &arr vs arr (same value, different types)
- Demonstrate pointer arithmetic differences
- Show how types affect arithmetic
- Decay Rules Test:
- Prove decay in expressions
- Show the three non-decay contexts
- Interactive examples
- Extern Mismatch Demonstration:
- Safely demonstrate the disaster
- Show memory contents
- Demonstrate the fix
- Lvalue Test:
- Show why arrays can’t be assigned
- Contrast with pointer modification
- Demonstrate with compiler errors
- Function Parameter Test:
- Show parameter decay
- Demonstrate 2D array handling
- Contrast correct and incorrect approaches
- String Literal Test:
- Compare
char s[]vschar *s - Show memory location differences
- Demonstrate modification behavior
- Compare
Non-Functional Requirements
- Educational: Clear explanations with each test
- Interactive: Allow running individual tests
- Visual: ASCII diagrams where helpful
- Safe: Don’t actually crash, simulate disasters
Real World Outcome
$ ./array_pointer_lab --all
╔════════════════════════════════════════════════════════════════╗
║ ARRAYS VS POINTERS: THE DEFINITIVE PROOF ║
╚════════════════════════════════════════════════════════════════╝
┌────────────────────────────────────────────────────────────────┐
│ TEST 1: sizeof Behavior │
├────────────────────────────────────────────────────────────────┤
│ │
│ int arr[10]; │
│ int *ptr = arr; │
│ │
│ sizeof(arr) = 40 bytes (10 elements × 4 bytes) │
│ sizeof(ptr) = 8 bytes (pointer size on 64-bit) │
│ │
│ ✓ PROOF: Arrays and pointers have different sizes │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ TEST 2: Address-of Operator │
├────────────────────────────────────────────────────────────────┤
│ │
│ int arr[10]; │
│ │
│ arr = 0x7fff5fbff8d0 (decays to int*) │
│ &arr = 0x7fff5fbff8d0 (type: int(*)[10]) │
│ &arr[0] = 0x7fff5fbff8d0 (type: int*) │
│ │
│ Same address, but: │
│ arr + 1 = 0x7fff5fbff8d4 (+4 bytes, one int) │
│ &arr + 1 = 0x7fff5fbff8f8 (+40 bytes, entire array!) │
│ │
│ ✓ PROOF: &arr and arr have same value but different types │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ TEST 3: Array Decay Contexts │
├────────────────────────────────────────────────────────────────┤
│ │
│ int arr[10]; │
│ │
│ Decay happens: │
│ - In expressions: arr + 1 (arr → &arr[0]) │
│ - In function calls: func(arr) │
│ - In pointer initialization: int *p = arr │
│ │
│ Decay does NOT happen: │
│ - With sizeof: sizeof(arr) = 40 (not 8) │
│ - With &: &arr gives pointer-to-array │
│ - String initializer: char s[] = "hi" creates array │
│ │
│ ✓ PROOF: Decay is context-dependent, not universal │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ TEST 4: Extern Mismatch Simulation │
├────────────────────────────────────────────────────────────────┤
│ │
│ // file1.c: char arr[] = "hello"; │
│ // file2.c: extern char *arr; // WRONG! │
│ │
│ Memory layout of arr: │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │ 0x68 │ 0x65 │ 0x6C │ 0x6C │ 0x6F │ 0x00 │ │
│ │ 'h' │ 'e' │ 'l' │ 'l' │ 'o' │ \0 │ │
│ └──────┴──────┴──────┴──────┴──────┴──────┘ │
│ │
│ If read as pointer (little-endian): │
│ First 8 bytes interpreted as address: 0x00006F6C6C6568 │
│ Dereferencing this CRASHES! │
│ │
│ ✓ PROOF: Type mismatch at linkage causes disasters │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ TEST 5: Lvalue Properties │
├────────────────────────────────────────────────────────────────┤
│ │
│ int arr[10], arr2[10]; │
│ int *ptr; │
│ │
│ ptr = arr; // OK - pointer assignment │
│ ptr++; // OK - pointer can be modified │
│ ptr = arr2; // OK - pointer can be reassigned │
│ │
│ arr = arr2; // ERROR - array assignment not allowed │
│ arr++; // ERROR - array cannot be modified │
│ │
│ ✓ PROOF: Arrays are non-modifiable lvalues │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ TEST 6: Function Parameters │
├────────────────────────────────────────────────────────────────┤
│ │
│ void test_func(int arr[100]) { │
│ printf("sizeof(arr) = %zu\n", sizeof(arr)); │
│ } │
│ │
│ int main() { │
│ int arr[100]; │
│ printf("sizeof(arr) = %zu\n", sizeof(arr)); // 400 │
│ test_func(arr); // 8! │
│ } │
│ │
│ Output: │
│ In main(): sizeof(arr) = 400 │
│ In func(): sizeof(arr) = 8 │
│ │
│ ✓ PROOF: Array parameters decay to pointers │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ TEST 7: String Literals │
├────────────────────────────────────────────────────────────────┤
│ │
│ char s1[] = "hello"; // Array on stack │
│ char *s2 = "hello"; // Pointer to .rodata │
│ │
│ sizeof(s1) = 6 // Array size includes \0 │
│ sizeof(s2) = 8 // Pointer size │
│ │
│ s1[0] = 'H'; // OK - modifiable │
│ s2[0] = 'H'; // UNDEFINED BEHAVIOR! │
│ │
│ Memory locations: │
│ s1 at: 0x7fff5fbff8d0 (stack) │
│ s2 points to: 0x10000f00 (read-only segment) │
│ │
│ ✓ PROOF: char[] creates array, char* creates pointer │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ TEST 8: 2D Arrays vs Pointer-to-Pointer │
├────────────────────────────────────────────────────────────────┤
│ │
│ int arr[3][4]; // Contiguous 12 ints │
│ int **pp; // Pointer to pointer │
│ │
│ Memory layout of int arr[3][4]: │
│ ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐ │
│ │ 00 │ 01 │ 02 │ 03 │ 10 │ 11 │ 12 │ 13 │ 20 │ 21 │ 22 │ 23 │ │
│ └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘ │
│ ← row 0 → ← row 1 → ← row 2 → │
│ │
│ Memory layout of int **pp: │
│ ┌────┐ │
│ │ pp │ → ┌────┐ ┌─────────────────┐ │
│ └────┘ │ p0 │ → │ row 0 elements │ │
│ ├────┤ ├─────────────────┤ │
│ │ p1 │ → │ row 1 elements │ (possibly elsewhere!) │
│ ├────┤ ├─────────────────┤ │
│ │ p2 │ → │ row 2 elements │ │
│ └────┘ └─────────────────┘ │
│ │
│ ✓ PROOF: 2D arrays are contiguous; int** is not │
└────────────────────────────────────────────────────────────────┘
╔════════════════════════════════════════════════════════════════╗
║ SUMMARY: Arrays and Pointers are FUNDAMENTALLY DIFFERENT ║
╠════════════════════════════════════════════════════════════════╣
║ 1. sizeof returns different values ║
║ 2. & operator produces different types ║
║ 3. Decay is context-dependent ║
║ 4. Extern mismatch causes crashes ║
║ 5. Arrays are non-modifiable lvalues ║
║ 6. Array parameters decay in functions ║
║ 7. char[] vs char* have different semantics ║
║ 8. 2D arrays are contiguous; pointer-to-pointer is not ║
╚════════════════════════════════════════════════════════════════╝
All tests passed! Arrays and pointers are DIFFERENT TYPES.
Solution Architecture
Project Structure
array_pointer_lab/
├── src/
│ ├── main.c # Entry point, CLI parsing
│ ├── sizeof_test.c # Test 1: sizeof behavior
│ ├── address_test.c # Test 2: address-of operator
│ ├── decay_test.c # Test 3: decay contexts
│ ├── extern_test.c # Test 4: extern mismatch
│ ├── lvalue_test.c # Test 5: lvalue properties
│ ├── param_test.c # Test 6: function parameters
│ ├── string_test.c # Test 7: string literals
│ └── twodim_test.c # Test 8: 2D arrays
├── include/
│ └── tests.h # Test function declarations
├── Makefile
└── README.md
Key Data Structures
/* Test result structure */
typedef struct {
const char *name;
int passed;
char description[256];
} TestResult;
/* Test function signature */
typedef TestResult (*TestFunc)(void);
/* Test registry */
typedef struct {
const char *name;
TestFunc func;
const char *description;
} TestEntry;
/* Memory visualization helpers */
typedef struct {
void *address;
size_t size;
const char *name;
const char *type_name;
} MemoryBlock;
Core Algorithm: Memory Visualization
/* Print memory contents as hex and ASCII */
void dump_memory(const void *ptr, size_t size, const char *label) {
printf("%s at %p (%zu bytes):\n", label, ptr, size);
const unsigned char *bytes = (const unsigned char *)ptr;
for (size_t i = 0; i < size; i++) {
if (i % 16 == 0) printf(" %04zx: ", i);
printf("%02x ", bytes[i]);
if ((i + 1) % 16 == 0 || i == size - 1) {
// Print ASCII representation
size_t start = i - (i % 16);
size_t end = i;
printf(" |");
for (size_t j = start; j <= end; j++) {
printf("%c", isprint(bytes[j]) ? bytes[j] : '.');
}
printf("|\n");
}
}
}
Implementation Guide
Phase 1: Basic Framework (2 hours)
Goal: Set up project structure and CLI parsing.
/* main.c */
#include <stdio.h>
#include <string.h>
#include "tests.h"
void print_usage(void) {
printf("Usage: array_pointer_lab [--test <name> | --all]\n");
printf("Tests:\n");
printf(" sizeof - sizeof behavior differences\n");
printf(" address - address-of operator behavior\n");
printf(" decay - array decay contexts\n");
printf(" extern - extern mismatch simulation\n");
printf(" lvalue - lvalue properties\n");
printf(" param - function parameter decay\n");
printf(" string - string literal differences\n");
printf(" twodim - 2D array vs pointer-to-pointer\n");
printf(" all - run all tests\n");
}
int main(int argc, char *argv[]) {
if (argc < 2) {
print_usage();
return 1;
}
if (strcmp(argv[1], "--test") == 0 && argc >= 3) {
run_test(argv[2]);
} else if (strcmp(argv[1], "--all") == 0) {
run_all_tests();
} else {
print_usage();
return 1;
}
return 0;
}
Phase 2: sizeof Test (1 hour)
/* sizeof_test.c */
#include <stdio.h>
#include "tests.h"
TestResult test_sizeof(void) {
TestResult result = { .name = "sizeof", .passed = 1 };
int arr[10];
int *ptr = arr;
printf("┌────────────────────────────────────────────────────────────────┐\n");
printf("│ TEST 1: sizeof Behavior │\n");
printf("├────────────────────────────────────────────────────────────────┤\n");
printf("│ │\n");
printf("│ int arr[10]; │\n");
printf("│ int *ptr = arr; │\n");
printf("│ │\n");
printf("│ sizeof(arr) = %2zu bytes (10 elements x %zu bytes) │\n",
sizeof(arr), sizeof(int));
printf("│ sizeof(ptr) = %2zu bytes (pointer size on 64-bit) │\n",
sizeof(ptr));
printf("│ │\n");
if (sizeof(arr) != sizeof(ptr)) {
printf("│ ✓ PROOF: Arrays and pointers have different sizes │\n");
result.passed = 1;
} else {
printf("│ ✗ Unexpected: sizes are equal │\n");
result.passed = 0;
}
printf("└────────────────────────────────────────────────────────────────┘\n\n");
return result;
}
Phase 3: Address-of Test (1 hour)
/* address_test.c */
#include <stdio.h>
#include "tests.h"
TestResult test_address(void) {
TestResult result = { .name = "address", .passed = 1 };
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
printf("┌────────────────────────────────────────────────────────────────┐\n");
printf("│ TEST 2: Address-of Operator │\n");
printf("├────────────────────────────────────────────────────────────────┤\n");
printf("│ │\n");
printf("│ int arr[10]; │\n");
printf("│ │\n");
printf("│ arr = %p (decays to int*) │\n", (void*)arr);
printf("│ &arr = %p (type: int(*)[10]) │\n", (void*)&arr);
printf("│ &arr[0] = %p (type: int*) │\n", (void*)&arr[0]);
printf("│ │\n");
printf("│ Same address, but: │\n");
printf("│ arr + 1 = %p (+%zu bytes, one int) │\n",
(void*)(arr + 1), sizeof(int));
printf("│ &arr + 1 = %p (+%zu bytes, entire array!) │\n",
(void*)(&arr + 1), sizeof(arr));
printf("│ │\n");
// Verify same value
if ((void*)arr == (void*)&arr) {
printf("│ ✓ PROOF: &arr and arr have same value but different types │\n");
}
printf("└────────────────────────────────────────────────────────────────┘\n\n");
return result;
}
Phase 4: Extern Mismatch Simulation (1 hour)
/* extern_test.c */
#include <stdio.h>
#include <string.h>
#include "tests.h"
/* Simulate the extern mismatch without actually crashing */
TestResult test_extern_mismatch(void) {
TestResult result = { .name = "extern", .passed = 1 };
/* This is what file1.c would define */
char arr[] = "hello";
printf("┌────────────────────────────────────────────────────────────────┐\n");
printf("│ TEST 4: Extern Mismatch Simulation │\n");
printf("├────────────────────────────────────────────────────────────────┤\n");
printf("│ │\n");
printf("│ // file1.c: char arr[] = \"hello\"; │\n");
printf("│ // file2.c: extern char *arr; // WRONG! │\n");
printf("│ │\n");
printf("│ Memory layout of arr: │\n");
printf("│ ┌──────┬──────┬──────┬──────┬──────┬──────┐ │\n");
printf("│ │ 0x%02x │ 0x%02x │ 0x%02x │ 0x%02x │ 0x%02x │ 0x%02x │ │\n",
(unsigned char)arr[0], (unsigned char)arr[1],
(unsigned char)arr[2], (unsigned char)arr[3],
(unsigned char)arr[4], (unsigned char)arr[5]);
printf("│ │ '%c' │ '%c' │ '%c' │ '%c' │ '%c' │ \\0 │ │\n",
arr[0], arr[1], arr[2], arr[3], arr[4]);
printf("│ └──────┴──────┴──────┴──────┴──────┴──────┘ │\n");
printf("│ │\n");
/* Show what would happen if misinterpreted as pointer */
unsigned long fake_ptr;
memcpy(&fake_ptr, arr, sizeof(fake_ptr));
printf("│ If read as pointer (little-endian): │\n");
printf("│ First 8 bytes interpreted as address: 0x%016lx │\n", fake_ptr);
printf("│ Dereferencing this CRASHES! │\n");
printf("│ │\n");
printf("│ ✓ PROOF: Type mismatch at linkage causes disasters │\n");
printf("└────────────────────────────────────────────────────────────────┘\n\n");
return result;
}
Phase 5: String Literal Test (1 hour)
/* string_test.c */
#include <stdio.h>
#include "tests.h"
TestResult test_string_literals(void) {
TestResult result = { .name = "string", .passed = 1 };
char s1[] = "hello"; /* Array on stack */
char *s2 = "hello"; /* Pointer to .rodata */
printf("┌────────────────────────────────────────────────────────────────┐\n");
printf("│ TEST 7: String Literals │\n");
printf("├────────────────────────────────────────────────────────────────┤\n");
printf("│ │\n");
printf("│ char s1[] = \"hello\"; // Array on stack │\n");
printf("│ char *s2 = \"hello\"; // Pointer to .rodata │\n");
printf("│ │\n");
printf("│ sizeof(s1) = %zu // Array size includes \\0 │\n", sizeof(s1));
printf("│ sizeof(s2) = %zu // Pointer size │\n", sizeof(s2));
printf("│ │\n");
printf("│ s1[0] = 'H'; // OK - modifiable │\n");
printf("│ s2[0] = 'H'; // UNDEFINED BEHAVIOR! │\n");
printf("│ │\n");
printf("│ Memory locations: │\n");
printf("│ s1 at: %p (stack) │\n", (void*)s1);
printf("│ s2 points to: %p (read-only segment) │\n", (void*)s2);
printf("│ │\n");
/* Demonstrate modifiability */
s1[0] = 'H'; /* This is safe */
printf("│ After s1[0] = 'H': s1 = \"%s\" │\n", s1);
printf("│ │\n");
printf("│ ✓ PROOF: char[] creates array, char* creates pointer │\n");
printf("└────────────────────────────────────────────────────────────────┘\n\n");
return result;
}
Phase 6: 2D Array Test (1 hour)
/* twodim_test.c */
#include <stdio.h>
#include <stdlib.h>
#include "tests.h"
TestResult test_2d_arrays(void) {
TestResult result = { .name = "twodim", .passed = 1 };
int arr[3][4]; /* Contiguous 2D array */
/* Fill with recognizable values */
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
arr[i][j] = i * 10 + j;
}
}
printf("┌────────────────────────────────────────────────────────────────┐\n");
printf("│ TEST 8: 2D Arrays vs Pointer-to-Pointer │\n");
printf("├────────────────────────────────────────────────────────────────┤\n");
printf("│ │\n");
printf("│ int arr[3][4]; // Contiguous 12 ints │\n");
printf("│ │\n");
printf("│ Memory layout of int arr[3][4] (values shown): │\n");
printf("│ ");
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("[%02d]", arr[i][j]);
}
if (i < 2) printf(" ");
}
printf(" │\n");
printf("│ ← row 0 → ← row 1 → ← row 2 → │\n");
printf("│ │\n");
printf("│ Address verification (contiguous): │\n");
printf("│ &arr[0][0] = %p │\n", (void*)&arr[0][0]);
printf("│ &arr[0][3] = %p (+12 bytes) │\n", (void*)&arr[0][3]);
printf("│ &arr[1][0] = %p (+16 bytes from start) │\n", (void*)&arr[1][0]);
printf("│ │\n");
printf("│ ✓ PROOF: 2D arrays are contiguous; int** is not │\n");
printf("└────────────────────────────────────────────────────────────────┘\n\n");
return result;
}
Hints in Layers
Hint 1: Understanding sizeof
The sizeof operator is evaluated at compile time (mostly). For arrays, the compiler knows the full size. For pointers, it only knows the pointer size.
int arr[10];
printf("%zu\n", sizeof(arr)); // 40 - compiler knows array size
printf("%zu\n", sizeof(arr[0])); // 4 - size of one element
/* In a function, arrays decay */
void func(int arr[10]) {
printf("%zu\n", sizeof(arr)); // 8! Parameter is pointer
}
Hint 2: Visualizing Memory
Use a helper function to dump memory in a visual way:
void hexdump(const void *ptr, size_t len) {
const unsigned char *p = ptr;
for (size_t i = 0; i < len; i++) {
printf("%02x ", p[i]);
if ((i + 1) % 16 == 0) printf("\n");
}
printf("\n");
}
Hint 3: Simulating Disasters Safely
Don’t actually crash! Show what WOULD happen:
/* Don't do this */
// char *wrong = (char *)0x12345678;
// printf("%s", wrong); // CRASH!
/* Do this instead */
char arr[] = "hello";
unsigned long as_ptr;
memcpy(&as_ptr, arr, sizeof(as_ptr));
printf("Would dereference: %p (CRASH!)\n", (void*)as_ptr);
Hint 4: Pointer Arithmetic Differences
Show how pointer arithmetic differs based on type:
int arr[10];
int *p1 = arr; // p1 + 1 moves 4 bytes
int (*p2)[10] = &arr; // p2 + 1 moves 40 bytes!
printf("arr + 1 = %p\n", (void*)(arr + 1)); // +4
printf("&arr + 1 = %p\n", (void*)(&arr + 1)); // +40
Testing Strategy
Test Matrix
| Test | What It Proves | Success Criteria |
|---|---|---|
| sizeof | Types differ | sizeof(arr) != sizeof(ptr) |
| address | Types differ | &arr + 1 != arr + 1 |
| decay | Context-dependent | sizeof works, & works |
| extern | Linkage matters | Simulated crash shown |
| lvalue | Assignment fails | Compiler errors mentioned |
| param | Decay in functions | sizeof differs in func |
| string | char[] vs char* | Different locations |
| twodim | Layout differs | Contiguous proven |
Validation Commands
# Compile with warnings
gcc -Wall -Wextra -pedantic -std=c11 -o array_pointer_lab *.c
# Run all tests
./array_pointer_lab --all
# Run specific test
./array_pointer_lab --test sizeof
# Verify with objdump (for extern test)
objdump -t *.o | grep arr
Common Pitfalls & Debugging
| Pitfall | Symptom | Solution |
|---|---|---|
| Forgetting decay | sizeof gives wrong value | Remember: sizeof doesn’t decay |
| 2D array confusion | int** doesn’t work for int[][] | Use int (*)[N] for 2D |
| String literal modification | Segfault | Use char[] for modifiable strings |
| Extern mismatch | Mysterious crashes | Match declarations exactly |
| VLA sizeof | Runtime vs compile-time | VLAs have runtime sizeof |
Extensions & Challenges
Beginner Extensions
- Add tests for
volatilearrays/pointers - Test with different types (double, struct)
- Add compiler error demonstration (try to compile bad code)
Intermediate Extensions
- Multi-file extern test (actual linking)
- VLA (Variable Length Array) tests
- Pointer-to-function vs array-of-function-pointers
Advanced Extensions
- Create a “type checker” that validates array/pointer usage
- Integrate with Valgrind to show actual memory behavior
- Add assembly output comparison
Real-World Connections
Common Bugs This Prevents
- Buffer overflow from wrong sizeof:
void copy(char *dest, char src[100]) { memcpy(dest, src, sizeof(src)); // BUG: copies 8 bytes, not 100! } - String literal modification crash:
char *msg = "hello"; msg[0] = 'H'; // CRASH! - 2D array function parameter errors:
void process(int **matrix); // WRONG for int arr[M][N] void process(int (*matrix)[N]); // CORRECT
Interview Questions
- “Are arrays and pointers the same in C?”
- Answer: No. Arrays are contiguous memory blocks; pointers are variables holding addresses.
- “What is array decay?”
- Answer: Implicit conversion of array to pointer to first element in most contexts.
- “When does array decay NOT happen?”
- Answer: sizeof, &, and string literal initialization.
- “What happens with
extern char *svsextern char s[]?”- Answer: Type mismatch causes linker to interpret memory incorrectly.
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Array/Pointer Relationship | Expert C Programming | Ch. 4, 9, 10 |
| Memory Layout | CS:APP | Ch. 3, 7 |
| Type System | The C Programming Language | Ch. 5 |
| Common Mistakes | C Traps and Pitfalls | Ch. 2 |
Self-Assessment Checklist
Understanding
- I can explain why sizeof(arr) != sizeof(ptr)
- I know the three contexts where arrays don’t decay
- I can explain the extern mismatch disaster
- I understand why arrays are non-modifiable lvalues
- I know the difference between char[] and char*
Implementation
- All 8 tests pass and display correctly
- Output is clear and educational
- Memory visualization is accurate
- No actual crashes in the test suite
Growth
- I can spot array/pointer confusion in code reviews
- I write correct function signatures for array parameters
- I understand multi-dimensional array layouts
Submission / Completion Criteria
Minimum Viable Completion
- sizeof test works and proves difference
- Address test shows same value, different types
- String literal test demonstrates char[] vs char*
Full Completion
- All 8 tests implemented and passing
- Clear ASCII art output
- Educational explanations included
- No undefined behavior in test suite
Excellence
- Interactive exploration mode
- Compiler error demonstrations
- Multi-file linking demonstration
- Integration with debugging tools
This guide was expanded from EXPERT_C_PROGRAMMING_DEEP_DIVE.md. For the complete learning path, see the project index.