Array vs pointer in C: sizeof, char[] and char *

An array is not a pointer. sizeof, array decay, a[i] and *(a + i), char[] vs char *, 2D arrays, and the error assignment to expression with array type.

By
Published on

You write t[i] on an array and p[i] on a pointer, and both work. You pass an array to a function that expects a pointer, and it compiles. Then sizeof returns 8 instead of 20, or GCC rejects name = "Yasmine". Arrays and pointers look alike enough to get mixed up, and differ just enough for the mix-up to cost you.

In short

An array is a block of memory that holds its elements; a pointer is a variable that holds an address. In almost every expression, the name of an array is converted to a pointer to its first element, a conversion called "decay", which is why a[i] is written the same way for both. But sizeof on an array gives its total size (20 for int[5]) while sizeof on a pointer gives 8 on a 64-bit machine, an array passed to a function only arrives there as a pointer, and an array cannot be assigned: name = "Yasmine" gives assignment to expression with array type.

sizeof on an array vs sizeof on a pointer

On an array, sizeof gives the space taken by all of its elements; on a pointer, the space taken by one address, whatever that address points to.

size.cVisualize
#include <stdio.h>
int main(void){    int scores[5] = {12, 15, 9, 18, 11};    int *p = scores;
    printf("sizeof scores: %zu\n", sizeof scores);    printf("sizeof p:      %zu\n", sizeof p);    printf("elements:      %zu\n", sizeof scores / sizeof scores[0]);    return 0;}
Terminal
$ gcc -Wall -Wextra -g size.c -o size$ ./sizesizeof scores: 20sizeof p:      8elements:      5

scores takes 5 times 4 bytes. p only holds an address, 8 bytes on a 64-bit machine, even though it points to the first element of scores. Dividing sizeof scores / sizeof scores[0] is the classic way to count the elements of an array. It only works where the array is declared, and that is where the trouble starts.

Why sizeof returns 8 inside a function

Because a function never receives an array. A parameter declared as int scores[5] is rewritten by the compiler as int *scores, and the 5 is ignored. GCC tells you, even without -Wall:

param.c
#include <stdio.h>
void print_size(int scores[5]){    printf("in the function: %zu\n", sizeof scores);}
int main(void){    int scores[5] = {12, 15, 9, 18, 11};
    printf("in main: %zu\n", sizeof scores);    print_size(scores);    return 0;}
Terminal
$ gcc -Wall -Wextra -g param.c -o paramparam.c: In function 'print_size':param.c:5:45: warning: 'sizeof' on array function parameter 'scores' will return size of 'int *' [-Wsizeof-array-argument]    5 |     printf("in the function: %zu\n", sizeof scores);      |                                             ^~~~~~param.c:3:21: note: declared here    3 | void print_size(int scores[5])      |                 ~~~~^~~~~~~~~$ ./paramin main: 20in the function: 8

The warning means exactly what it says: sizeof on a parameter declared as an array will return the size of an int *. In a real program, the mistake is harder to spot. This function computes an average and counts its scores with the formula from the previous section:

average.cAvoid
#include <stdio.h>
int average(int scores[]){    int n = sizeof scores / sizeof scores[0];    int total = 0;
    for (int i = 0; i < n; i++)        total += scores[i];    return total / n;}
int main(void){    int scores[5] = {12, 15, 9, 18, 6};
    printf("%d\n", average(scores));    return 0;}
Terminal
$ ./average13

The real average is 12. n is 8 / 4, so 2, and the function only read the first two scores. No crash and a plausible result: the worst kind of bug. There is only one fix, passing the length as a parameter: int average(const int *scores, size_t n). That is why qsort, memcpy and fgets always ask for a size next to the pointer.

C

is_sorted

is_sorted receives a pointer and an element count n, like any C function that works on an array. Read exactly n values, never a[n], and report whether they are in ascending order. The statement insists on n equal to 0, equal neighbors, and INT_MIN placed right before INT_MAX.

Open the exercise

Array decay: when an array becomes a pointer

In an expression, the name of an array is converted to a pointer to its first element. int *p = scores; means the same as int *p = &scores[0];, and the same conversion happens when you pass scores to a function. The standard lists a few exceptions: the operand of sizeof, the operand of &, the operand of typeof since C23, and a string literal that initializes a char array.

Decay also explains indexing. By definition, a[i] is exactly *(a + i): move the address forward by i elements, then read what is there. The offset counts elements, not bytes. Since addition is commutative, 2[scores] is valid code. Nobody should write it, but it proves the mechanism:

addresses.cVisualize
#include <stdio.h>
int main(void){    int scores[5] = {12, 15, 9, 18, 11};
    printf("scores      = %p\n", (void *)scores);    printf("&scores     = %p\n", (void *)&scores);    printf("scores + 1  = %p\n", (void *)(scores + 1));    printf("&scores + 1 = %p\n", (void *)(&scores + 1));    printf("scores[2] = %d, *(scores + 2) = %d, 2[scores] = %d\n",           scores[2], *(scores + 2), 2[scores]);    return 0;}
Terminal
$ gcc -Wall -Wextra -g addresses.c -o addresses$ ./addressesscores      = 0xffffd3ebe528&scores     = 0xffffd3ebe528scores + 1  = 0xffffd3ebe52c&scores + 1 = 0xffffd3ebe53cscores[2] = 9, *(scores + 2) = 9, 2[scores] = 9

The addresses change on every run; the gaps between them don't. Look at the first two lines: scores and &scores print the same address, the start of the array. Their types differ, though. scores decays to int *, a pointer to one integer, and scores + 1 moves 4 bytes ahead (528 becomes 52c). &scores is an int (*)[5], a pointer to the whole array, and &scores + 1 moves 20 bytes ahead (528 becomes 53c), right past the last element.

C

sum_range

sum_range adds up the integers between two pointers, first included and last excluded, with no length parameter at all. Pointer arithmetic is the only way to move forward, and you must never read the slot that last points to.

Open the exercise

char s[] vs char *s: the difference

char arr[] = "hello"; creates an array of 6 char and copies the text into it. char *ptr = "hello"; creates a pointer to a string literal that the compiler stores somewhere else. Both print the same with %s, and everything else about them differs:

strings.cVisualize
#include <stdio.h>
int main(void){    char arr[] = "hello";    char *ptr = "hello";
    printf("sizeof arr: %zu\n", sizeof arr);    printf("sizeof ptr: %zu\n", sizeof ptr);    arr[0] = 'H';    printf("%s\n", arr);    ptr = "goodbye";    printf("%s\n", ptr);    return 0;}
Terminal
$ gcc -Wall -Wextra -g strings.c -o strings$ ./stringssizeof arr: 6sizeof ptr: 8Hellogoodbye
Right after the two declarations

Stack

main()
char[6]arr
'h''e''l''l''o''\0'
char *ptr
literal

Static data

literalchar[6]read-only
'h''e''l''l''o''\0'
char arr[] = "hello";char *ptr = "hello";
What it isan array of 6 char, on the stacka pointer to a string literal
sizeof6, the text plus its '\0'8, the size of an address
Change one characteryes, arr[0] = 'H'no, the string literal is read-only
Point to another textno, arr = ... doesn't compileyes, ptr = "goodbye"

Writing ptr[0] = 'H' compiles without a single warning and crashes at run time: it is one of the cases covered in Segmentation fault (core dumped) in C. The habit that prevents it is to declare const char *ptr = "hello";, so the compiler rejects any write. For how strings themselves work, '\0', strlen and strcpy, read Strings in C.

assignment to expression with array type

You get this error when you try to assign a whole array with =:

assign.cAvoid
#include <stdio.h>
int main(void){    char name[20];
    name = "Yasmine";    printf("%s\n", name);    return 0;}
Terminal
$ gcc -Wall -Wextra -g assign.c -o assignassign.c: In function 'main':assign.c:7:10: error: assignment to expression with array type    7 |     name = "Yasmine";      |          ^

An array cannot be the target of an assignment: the standard explicitly excludes array types from "modifiable lvalues", the things you are allowed to put on the left of =. name designates the 20-byte area itself. No address is stored anywhere, so there is nothing to replace. You have three options, depending on what you want:

Rejected by the compilerAvoid
char name[20];name = "Yasmine";
Initialize, copy or pointCorrect
char name[20] = "Yasmine";       /* at the declaration */strcpy(name, "Ines");            /* copy, if it fits */const char *nick = "Yas";        /* a pointer can be */nick = "Ine";                    /* reassigned */

The initialization char name[20] = "Yasmine"; also uses =, but the standard treats it as a separate case: a string literal can fill an array at the moment you declare it, and only then. The same rule forbids a = b between two int arrays. Copy them with a loop or with memcpy(3).

Two-dimensional arrays

A 2D array is an array of arrays, stored row after row in a single block. int grid[2][3] holds six contiguous int:

int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};

Stack

main()
int[2][3]grid
123456
row 0, then row 1

Decay only applies to the first level: grid becomes a pointer to its first row, of type int (*)[3]. That is not an int **, a pointer to pointers, and since GCC 14 mixing the two up is an error:

Terminal
$ gcc -Wall -Wextra -g matrix.c -o matrixmatrix.c: In function 'main':matrix.c:17:24: error: passing argument 1 of 'sum' from incompatible pointer type [-Wincompatible-pointer-types]   17 |     printf("%d\n", sum(grid, 2, 3));      |                        ^~~~      |                        |      |                        int (*)[3]matrix.c:3:15: note: expected 'int **' but argument is of type 'int (*)[3]'    3 | int sum(int **m, int rows, int cols)      |         ~~~~~~^

An int ** expects an array of addresses in memory, one per row. The grid contains none, only six integers in a row. If the compiler let this through, m[0] would read the bytes of 1 and 2 as an address on a 64-bit machine. The function needs the width of a row to work out where the next row starts, so the parameter spells it out:

matrix2.c
#include <stdio.h>
int sum(int m[][3], int rows){    int total = 0;
    for (int i = 0; i < rows; i++)        for (int j = 0; j < 3; j++)            total += m[i][j];    return total;}
int main(void){    int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
    printf("%d\n", sum(grid, 2));    printf("sizeof grid: %zu, sizeof grid[0]: %zu\n",           sizeof grid, sizeof grid[0]);    return 0;}
Terminal
$ gcc -Wall -Wextra -g matrix2.c -o matrix2$ ./matrix221sizeof grid: 24, sizeof grid[0]: 12

int m[][3] and int (*m)[3] are two ways of writing the same parameter. grid takes 24 bytes, one row 12. The Arrays and matrices module of the C path has you practice these walks over rows and columns, up to filling a grid.

Frequently asked questions

Is an array a constant pointer?

No, even though the comparison gets repeated a lot. An array is an object that contains its elements: sizeof gives their total size, and &arr is a pointer to the whole array. A constant pointer is a variable that holds an address, 8 bytes on a 64-bit machine. The name of an array converts to a pointer in most expressions, which does not make it a pointer.

How do I get the size of an array passed as a parameter?

You can't: the function only receives a pointer, and the size is lost. Pass it in a parameter next to it (size_t n), or pass an end pointer, as in sum_range(first, last). Strings are the exception: their '\0' marks the end.

Can a function return an array?

No, a function's return type cannot be an array. Return a pointer to a block allocated with malloc, which the caller will free, or have the function fill an array that the caller provides. Never return the address of a local array: it disappears at return.

Why do printf("%s", arr) and printf("%s", ptr) print the same thing?

Because arr decays to char * at the call. printf receives an address in both cases and reads up to the '\0'. The difference between the two only exists where they are declared.

Sources

Now write it yourself.