C Pointers and Arrays
Arrays and pointers are closely connected in C, but they are not the same thing. Understanding the relationship explains why array parameters behave like pointers, why a[i] works the way it does, and why functions need a separate length when they receive an array.
This lesson focuses on the exact boundary between an actual array object and a pointer to one of its elements. That boundary is one of the most important ideas in practical C programming.
Overview: How Pointers and Arrays Work
An array is an object that contains a fixed number of elements stored contiguously. If you declare int scores[4], C creates four adjacent int objects named scores[0] through scores[3]. The array object has a total size of 4 * sizeof(int), and the compiler knows that size only in the scope where the real array is declared.
A pointer is a separate object that stores an address. A pointer can point at an array element, commonly the first one. In most expressions, an array name is automatically converted, or decays, to a pointer to its first element. For an array scores, the expression scores usually becomes &scores[0], which has type int *.
The word “usually” matters. Array-to-pointer conversion does not happen when the array is the operand of sizeof, the operand of unary &, or a string literal used to initialize a character array. Therefore, sizeof scores gives the total bytes of the array, but sizeof p for int *p = scores; gives the size of the pointer variable itself.
Indexing is also pointer-based. The expression scores[i] is defined as *(scores + i). First C finds the pointer to the first element, then moves i elements forward, then dereferences that position. Pointer arithmetic is scaled by the pointed-to type, so adding 1 to an int * moves to the next int, not necessarily the next byte.
When an array is passed to a function, the parameter is adjusted to a pointer. A function parameter written as int values[] is really treated as int *values. Because the function receives only a pointer to the first element, it cannot discover the number of elements with sizeof values. Pass the element count separately, usually as size_t count.
There is one more important type distinction: scores in an expression usually becomes an int *, but &scores has type int (*)[4], a pointer to the whole array of four integers. Both may have the same numeric address on many machines, but their types and arithmetic are different. scores + 1 points to the second element; &scores + 1 points just past the whole array.
Syntax
type array_name[count];
type *pointer = array_name;
array_name[index]
*(pointer + index)
function(array_name, count)
| Form | Meaning |
|---|---|
int a[5]; |
Creates an actual array object containing five int elements. |
int *p = a; |
Stores a pointer to a[0] in p. |
a[i] |
Accesses element i; equivalent to *(a + i) in an expression. |
p[i] |
Accesses the element i positions after pointer p. |
sizeof a |
When a is a real array in scope, gives total array bytes. |
sizeof p |
Gives the size of the pointer variable, not the array it points into. |
void f(int a[], size_t n) |
A function receiving a pointer to the first element plus an explicit count. |
Examples
Array Name as First-Element Pointer
#include <stdio.h>
int main(void)
{
int scores[] = {70, 85, 90, 95};
int *p = scores;
printf("First with index: %d
", scores[0]);
printf("First with pointer: %d
", *p);
printf("Third with index: %d
", scores[2]);
printf("Third with pointer math: %d
", *(p + 2));
p[1] = 88;
printf("Changed second: %d
", scores[1]);
return 0;
}
Output:
First with index: 70
First with pointer: 70
Third with index: 90
Third with pointer math: 90
Changed second: 88
The array name scores converts to a pointer to scores[0] when used to initialize p. Both indexing and pointer arithmetic reach the same elements. Assigning through p[1] modifies scores[1] because there is only one underlying array element.
Passing an Array to a Function
#include <stdio.h>
#include <stddef.h>
int sum_array(const int values[], size_t count)
{
int total = 0;
for (size_t i = 0; i < count; i++) {
total += values[i];
}
return total;
}
int main(void)
{
int points[] = {4, 7, 6, 9, 3};
size_t count = sizeof points / sizeof points[0];
printf("Count: %zu
", count);
printf("Total: %d
", sum_array(points, count));
return 0;
}
Output:
Count: 5
Total: 29
Inside main, points is a real array, so sizeof points / sizeof points[0] computes the element count. Inside sum_array, the parameter values is a pointer even though it is written with brackets, so the function relies on the separate count argument.
Modifying an Array Through a Pointer Parameter
#include <stdio.h>
#include <stddef.h>
void add_bonus(int scores[], size_t count, int bonus)
{
for (size_t i = 0; i < count; i++) {
scores[i] += bonus;
}
}
void print_scores(const int *scores, size_t count)
{
for (size_t i = 0; i < count; i++) {
printf("%d", scores[i]);
if (i + 1 < count) {
printf(", ");
}
}
printf("
");
}
int main(void)
{
int scores[] = {81, 76, 90, 88};
size_t count = sizeof scores / sizeof scores[0];
add_bonus(scores, count, 5);
print_scores(scores, count);
return 0;
}
Output:
86, 81, 95, 93
The call add_bonus(scores, count, 5) passes a pointer to the first element. The function can write through that pointer, so the original array in main changes. The printing function uses const int * because it reads the elements without modifying them.
Pointer to the Whole Array
#include <stdio.h>
#include <stddef.h>
int main(void)
{
int row[3] = {10, 20, 30};
int *first = row;
int (*whole)[3] = &row;
printf("First element: %d
", *first);
printf("Second through whole array pointer: %d
", (*whole)[1]);
printf("Array bytes: %zu
", sizeof row);
printf("Pointer target bytes: %zu
", sizeof *whole);
return 0;
}
Output:
First element: 10
Second through whole array pointer: 20
Array bytes: 12
Pointer target bytes: 12
first points to one int. whole points to the entire array object whose type is int [3]. The expression (*whole)[1] first dereferences the pointer to recover the array, then indexes element 1. This distinction becomes especially useful with multidimensional arrays.
How It Works Step by Step
- The declaration
int row[3]creates one array object containing three adjacentintelements. - In an expression such as
row, the array usually converts to&row[0], a pointer to the first element. - The expression
row + 2moves twointelements forward, because the pointer type isint *. - The expression
row[2]is evaluated as*(row + 2), so it reads or writes the third element. - When
rowis passed to a function, the function receives a pointer value. It does not receive a copy of the entire array. - Because only the pointer is passed, writes through that pointer affect the caller’s array, but
sizeofinside the function measures the pointer parameter. - The expression
&rowcreates a pointer to the whole array, so arithmetic on that pointer moves by the size of the whole array object.
Common Mistakes
Using sizeof on an Array Parameter
This is wrong because the parameter is a pointer, not a real array: void f(int a[]) { size_t n = sizeof a / sizeof a[0]; }. Correct code computes the count where the actual array is still in scope and passes it: f(a, count);.
Assuming a Pointer Remembers the Array Length
A pointer stores an address, not a length. If int *p = scores;, there is no portable way to ask p how many elements remain. Keep the count beside the pointer, or use a begin/end pair where end points one past the last element.
Going One Element Too Far
For an array of count elements, valid indexes are 0 through count - 1. The pointer array + count is legal only as a one-past marker. Dereferencing it with *(array + count) or indexing array[count] is out of bounds and causes undefined behavior.
Assigning to an Array Name
An array name is not a modifiable pointer variable. Code such as scores = other; is invalid after scores has been declared as an array. Copy elements with a loop, or use a pointer variable if you need something that can be made to point elsewhere.
Best Practices
- Pass arrays to functions as a pointer plus a
size_telement count. - Compute
sizeof array / sizeof array[0]only wherearrayis a real array, not after it has decayed to a pointer. - Use
constin parameters such asconst int *valueswhen the function should only read elements. - Prefer indexing for simple position-based code and pointer ranges for algorithms that naturally walk from beginning to end.
- Never access indexes below
0or greater than or equal to the element count. - Keep pointer arithmetic within one array object, including the one-past position used for comparisons.
- Remember that
&arrayis a pointer to the whole array, not a pointer to the first element, even if the displayed address looks similar. - For multidimensional arrays, preserve the inner dimension in the parameter type, such as
int grid[][3]orint (*grid)[3].
Practice Exercises
- Write a function
double average(const int *values, size_t count)that returns the average of an integer array. Decide what to return whencountis zero. - Create an array of six integers and write a function that doubles every element through a pointer parameter. Print the array before and after the call.
- Write a program that uses
int *beginandint *endto count how many values in an array are greater than 50.
Summary
- An array is a real object containing contiguous elements; a pointer is a separate object that stores an address.
- In most expressions, an array name decays to a pointer to its first element.
a[i]is equivalent to*(a + i), so indexing is built on pointer arithmetic.- Array parameters are adjusted to pointer parameters, so functions need a separate count.
sizeofcan measure a real array in its declaring scope, but it measures only a pointer inside ordinary array parameters.- Writing through an array pointer parameter changes the caller’s array elements.
- Stay within valid bounds: the one-past pointer is for comparison, not dereferencing.
