ZetCode

C pointers

last modified November 14, 2026

A pointer is a variable that holds the address of another object. Pointers let a function change the caller's variables, give access to arrays and strings, refer to memory allocated at run time, and link structures together into lists and trees. They are also where most serious C bugs come from. This tutorial covers the address-of and dereference operators, passing pointers to functions, NULL, pointer arithmetic, the relation between arrays and pointers, const pointers, pointers to pointers, void pointers, pointers to structures, the lifetime of the object pointed to, and the errors that tools can detect.

Address-of and dereference

The unary & operator gives the address of an object, and the unary * operator, applied to a pointer, gives the object at that address. A pointer is declared with an asterisk before its name, and its type records the type of the object it points to.

address.c
#include <stdio.h>

int main(void) {

    int n = 25;
    int *p = &n;

    printf("value of n:      %d\n", n);
    printf("address of n:    %p\n", (void *) &n);
    printf("value of p:      %p\n", (void *) p);
    printf("value at *p:     %d\n", *p);
    printf("sizeof p:        %zu\n", sizeof p);

    return 0;
}

p is a pointer to int initialized with the address of n, so *p is n itself. The %p conversion expects a void * argument, hence the casts. The addresses are different on every run, because Linux places the stack at a random position for security (address space layout randomization). A pointer occupies 8 bytes on a 64-bit system regardless of the type it points to.

$ gcc -std=c17 -Wall -Wextra -o address address.c
$ ./address
value of n:      25
address of n:    0x7ffe030e0a74
value of p:      0x7ffe030e0a74
value at *p:     25
sizeof p:        8

Changing a value through a pointer

A dereferenced pointer can stand on the left side of an assignment like any variable. Every change made through *p is a change of the object p points to.

modify.c
#include <stdio.h>

int main(void) {

    int count = 10;
    int *p = &count;

    *p = 20;
    printf("count = %d\n", count);

    (*p)++;
    printf("count = %d\n", count);

    *p *= 2;
    printf("count = %d\n", count);

    return 0;
}

The three statements modify count without naming it. The parentheses in (*p)++ are required: postfix ++ binds tighter than *, so *p++ would increment the pointer and leave the value alone. Compound assignment such as *p *= 2 works as expected.

$ gcc -std=c17 -Wall -Wextra -o modify modify.c
$ ./modify
count = 20
count = 21
count = 42

Passing pointers to functions

C passes arguments by value: a function receives copies of the arguments and cannot change the caller's variables through them. Passing the addresses of the variables instead lets the function reach the originals.

swap.c
#include <stdio.h>

static void swap_values(int a, int b) {

    int tmp = a;
    a = b;
    b = tmp;
}

static void swap(int *a, int *b) {

    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void) {

    int x = 1, y = 2;

    swap_values(x, y);
    printf("after swap_values: x = %d, y = %d\n", x, y);

    swap(&x, &y);
    printf("after swap:        x = %d, y = %d\n", x, y);

    return 0;
}

swap_values exchanges its own copies, and the exchange is lost when it returns. swap receives two addresses and exchanges the values stored at them. The pointers themselves are still passed by value, but a copy of an address points to the same object as the original.

$ gcc -std=c17 -Wall -Wextra -o swap swap.c
$ ./swap
after swap_values: x = 1, y = 2
after swap:        x = 2, y = 1

Returning several values

A function returns a single value, but it can store further results through pointer parameters, often called output parameters. Many standard functions, such as strtol and scanf, work this way.

out_params.c
#include <stdio.h>

static void min_max(const int *vals, size_t n, int *min, int *max) {

    *min = *max = vals[0];

    for (size_t i = 1; i < n; i++) {
        if (vals[i] < *min) *min = vals[i];
        if (vals[i] > *max) *max = vals[i];
    }
}

int main(void) {

    int vals[] = { 4, -2, 17, 9, 0, 11 };
    int lo, hi;

    min_max(vals, sizeof vals / sizeof vals[0], &lo, &hi);

    printf("min: %d, max: %d\n", lo, hi);

    return 0;
}

min_max reads the array through a pointer to const int, which documents that it does not change the elements, and writes the two results through min and max. The caller passes the addresses of lo and hi, which do not need to be initialized because the function assigns them before reading them.

$ gcc -std=c17 -Wall -Wextra -o out_params out_params.c
$ ./out_params
min: -2, max: 17

The null pointer

NULL is a pointer value that points to no object. Functions return it to signal that there is no result, and a pointer that does not yet refer to anything is initialized with it. Dereferencing a null pointer is undefined behavior, so a pointer that may be null is tested first.

null_ptr.c
#include <stdio.h>
#include <string.h>

static const char *find_word(const char *words[], size_t n, const char *key) {

    for (size_t i = 0; i < n; i++) {
        if (strcmp(words[i], key) == 0) {
            return words[i];
        }
    }

    return NULL;
}

int main(void) {

    const char *words[] = { "red", "green", "blue" };
    const char *keys[] = { "green", "purple" };

    for (size_t i = 0; i < 2; i++) {

        const char *res = find_word(words, 3, keys[i]);

        if (res != NULL) {
            printf("%s: found at %p\n", keys[i], (void *) res);
        } else {
            printf("%s: not found\n", keys[i]);
        }
    }

    return 0;
}

find_word returns a pointer to the matching string or NULL. The caller compares the result with NULL before using it; if (res) is an equivalent and common shorthand. The address printed for the found word is low because it points to a string literal in the program's read-only data, not to the stack.

$ gcc -std=c17 -Wall -Wextra -o null_ptr null_ptr.c
$ ./null_ptr
green: found at 0x402008
purple: not found

Pointer arithmetic

Adding an integer to a pointer moves it by that many elements, not bytes. The compiler multiplies the integer by the size of the pointed-to type, so the same expression p + 1 advances by a different number of bytes for different types.

arithmetic.c
#include <stdio.h>

int main(void) {

    char c[3];
    int i[3];
    double d[3];

    char *pc = c;
    int *pi = i;
    double *pd = d;

    printf("char:   %p %p\n", (void *) pc, (void *) (pc + 1));
    printf("int:    %p %p\n", (void *) pi, (void *) (pi + 1));
    printf("double: %p %p\n", (void *) pd, (void *) (pd + 1));

    printf("byte steps: %td %td %td\n",
           (char *) (pc + 1) - (char *) pc,
           (char *) (pi + 1) - (char *) pi,
           (char *) (pd + 1) - (char *) pd);

    return 0;
}

The addresses are printed in hexadecimal: pc + 1 is one byte past pc, pi + 1 four bytes past pi and pd + 1 eight bytes past pd. Converting both pointers to char * before subtracting measures the distance in bytes, which confirms the sizes of char, int and double. Arithmetic is only defined within one array and up to one element past its end.

$ gcc -std=c17 -Wall -Wextra -o arithmetic arithmetic.c
$ ./arithmetic
char:   0x7ffe146944c5 0x7ffe146944c6
int:    0x7ffe146944b8 0x7ffe146944bc
double: 0x7ffe146944a0 0x7ffe146944a8
byte steps: 1 4 8

Walking an array with a pointer

Pointer arithmetic is the natural way to traverse an array: a pointer starts at the first element and is incremented until it reaches the end. The address one past the last element may be computed and compared, but not dereferenced.

traverse.c
#include <stdio.h>

int main(void) {

    int vals[] = { 3, 8, 1, 9, 4 };
    size_t n = sizeof vals / sizeof vals[0];

    for (size_t i = 0; i < n; i++) {
        printf("%d ", vals[i]);
    }

    printf("\n");

    for (int *p = vals; p < vals + n; p++) {
        printf("%d ", *p);
    }

    printf("\n");

    int *end = vals + n;
    int sum = 0;

    for (int *p = vals; p != end; p++) {
        sum += *p;
    }

    printf("sum: %d\n", sum);

    return 0;
}

The first loop uses an index, the second the condition p < vals + n, and the third an explicit end pointer. All three visit the same elements. The end-pointer form appears often in C code and is the model for iterator ranges in C++.

$ gcc -std=c17 -Wall -Wextra -o traverse traverse.c
$ ./traverse
3 8 1 9 4
3 8 1 9 4
sum: 25

Subtracting and comparing pointers

Subtracting two pointers into the same array gives the number of elements between them, as a value of type ptrdiff_t from <stddef.h>. Pointers into the same array can also be compared with the relational operators, and an index can be negative when the pointer is not at the start of the array.

difference.c
#include <stddef.h>
#include <stdio.h>

int main(void) {

    int vals[] = { 10, 20, 30, 40, 50, 60 };

    int *first = &vals[1];
    int *last = &vals[4];
    ptrdiff_t dist = last - first;

    printf("elements between: %td\n", dist);
    printf("last > first: %d\n", last > first);
    printf("*(first + 2): %d\n", *(first + 2));
    printf("first[2]: %d\n", first[2]);
    printf("last[-1]: %d\n", last[-1]);

    return 0;
}

first points to the element 20 and last to 50, so they are three elements apart. first[2] is defined as *(first + 2) and gives 40, and last[-1] steps one element back to the same value. %td is the printf conversion for ptrdiff_t.

$ gcc -std=c17 -Wall -Wextra -o difference difference.c
$ ./difference
elements between: 3
last > first: 1
*(first + 2): 40
first[2]: 40
last[-1]: 40

Arrays and pointers

An array is not a pointer, but in most expressions an array is converted to a pointer to its first element. This conversion, called decay, does not happen with sizeof and the address-of operator, and it always happens when an array is passed to a function.

decay.c
#include <stdio.h>

static void show(int arr[], size_t n) {

    printf("in show: sizeof arr = %zu\n", sizeof arr);

    for (size_t i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }

    printf("\n");
}

int main(void) {

    int vals[5] = { 1, 2, 3, 4, 5 };
    int *p = vals;

    printf("in main: sizeof vals = %zu\n", sizeof vals);
    printf("in main: sizeof p = %zu\n", sizeof p);
    printf("vals == &vals[0]: %d\n", vals == &vals[0]);
    printf("vals[2] = %d, *(vals + 2) = %d, 2[vals] = %d\n",
           vals[2], *(vals + 2), 2[vals]);

    show(vals, sizeof vals / sizeof vals[0]);

    return 0;
}

In main, sizeof vals is the size of the array, 20 bytes, while sizeof p is the size of a pointer. The subscript a[i] is defined as *(a + i), which is why the odd 2[vals] also works. The parameter int arr[] is really int *arr, so inside show the array size is lost; GCC warns about the misleading sizeof, and the length has to be passed as a separate argument.

$ gcc -std=c17 -Wall -Wextra -o decay decay.c
decay.c: In function ‘show’:
decay.c:5:50: warning: ‘sizeof’ on array function parameter ‘arr’ will return size of ‘int *’ [-Wsizeof-array-argument]
    5 |     printf("in show: sizeof arr = %zu\n", sizeof arr);
      |                                                  ^~~
decay.c:3:22: note: declared here
    3 | static void show(int arr[], size_t n) {
      |                  ~~~~^~~~~
$ ./decay
in main: sizeof vals = 20
in main: sizeof p = 8
vals == &vals[0]: 1
vals[2] = 3, *(vals + 2) = 3, 2[vals] = 3
in show: sizeof arr = 8
1 2 3 4 5

Pointers and const

const can apply to the object a pointer points to, to the pointer itself, or to both. Reading the declaration from right to left helps: const int *p is a pointer to a constant integer, and int *const p is a constant pointer to an integer.

const_ptr.c
#include <stdio.h>

static int sum(const int *vals, size_t n) {

    int total = 0;

    for (size_t i = 0; i < n; i++) {
        total += vals[i];
    }

    return total;
}

int main(void) {

    int a = 1, b = 2;
    int vals[] = { 1, 2, 3 };

    const int *pc = &a;
    pc = &b;

    int *const cp = &a;
    *cp = 10;

    const int *const cc = &b;

    printf("*pc = %d, *cp = %d, *cc = %d\n", *pc, *cp, *cc);
    printf("sum = %d\n", sum(vals, 3));

    return 0;
}

pc can be redirected from a to b, but *pc cannot be assigned. cp must stay pointed at a, but the value can be changed through it, and cc allows neither. The parameter of sum uses the first form, which is the most common use of const with pointers: it promises the caller that the function only reads the data.

$ gcc -std=c17 -Wall -Wextra -o const_ptr const_ptr.c
$ ./const_ptr
*pc = 2, *cp = 10, *cc = 2
sum = 6

Violating const

The compiler enforces the qualifiers. The next program tries the two forbidden operations: writing through a pointer to const and redirecting a const pointer.

const_error.c
#include <stdio.h>

int main(void) {

    int a = 1, b = 2;

    const int *pc = &a;
    *pc = 5;

    int *const cp = &a;
    cp = &b;

    printf("%d %d\n", *pc, *cp);

    return 0;
}

Both assignments are rejected with an error, so no executable is produced. The messages distinguish the two cases: *pc is a read-only location, while cp is a read-only variable.

$ gcc -std=c17 -Wall -Wextra -o const_error const_error.c
const_error.c: In function ‘main’:
const_error.c:8:9: error: assignment of read-only location ‘*pc’
    8 |     *pc = 5;
      |         ^
const_error.c:11:8: error: assignment of read-only variable ‘cp’
   11 |     cp = &b;
      |        ^

Pointers to pointers

A pointer is an object too, so it has an address that can be stored in a pointer to a pointer. The main use is a function that must change a pointer owned by the caller, for example to hand back newly allocated memory.

ptr_to_ptr.c
#include <stdio.h>
#include <stdlib.h>

static int make_array(int **out, size_t n) {

    int *arr = malloc(n * sizeof *arr);

    if (arr == NULL) {
        return -1;
    }

    for (size_t i = 0; i < n; i++) {
        arr[i] = (int) (i * i);
    }

    *out = arr;

    return 0;
}

int main(void) {

    int x = 7;
    int *p = &x;
    int **pp = &p;

    printf("x = %d, *p = %d, **pp = %d\n", x, *p, **pp);

    int *squares = NULL;

    if (make_array(&squares, 6) != 0) {
        perror("malloc");
        return 1;
    }

    for (size_t i = 0; i < 6; i++) {
        printf("%d ", squares[i]);
    }

    printf("\n");
    free(squares);

    return 0;
}

pp holds the address of p, so *pp is p and **pp is x. make_array receives the address of the caller's squares and stores the allocated block in it through *out; the return value is free to report success or failure. Had the parameter been a plain int *, the function would have changed only its own copy and leaked the memory.

$ gcc -std=c17 -Wall -Wextra -o ptr_to_ptr ptr_to_ptr.c
$ ./ptr_to_ptr
x = 7, *p = 7, **pp = 7
0 1 4 9 16 25

Arrays of pointers

The parameter argv of main is an array of pointers to the command-line arguments, and it decays to a char **. The standard guarantees that argv[argc] is a null pointer, so the array can also be walked without the count.

argv_ptrs.c
#include <stdio.h>

int main(int argc, char *argv[]) {

    for (char **p = argv; *p != NULL; p++) {
        printf("%td: %s\n", p - argv, *p);
    }

    printf("argc = %d, argv[argc] is %s\n", argc,
           argv[argc] == NULL ? "NULL" : "not NULL");

    return 0;
}

The loop variable p points to the elements of argv in turn, so *p is one argument string, and p - argv is its index. The loop stops at the terminating null pointer. The first argument is the name the program was started with.

$ gcc -std=c17 -Wall -Wextra -o argv_ptrs argv_ptrs.c
$ ./argv_ptrs red green
0: ./argv_ptrs
1: red
2: green
argc = 3, argv[argc] is NULL

Pointers to arrays

A two-dimensional array is an array of rows, and it decays to a pointer to its first row. The type of that pointer, int (*)[4], includes the row length, so incrementing it skips a whole row. The parentheses matter: int *row[4] would be an array of four pointers.

two_dim.c
#include <stdio.h>

static void print_matrix(int rows, int cols, int (*m)[cols]) {

    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            printf("%3d", m[r][c]);
        }
        printf("\n");
    }
}

int main(void) {

    int grid[3][4] = {
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }
    };

    int (*row)[4] = grid;

    printf("sizeof *row: %zu\n", sizeof *row);
    printf("row[1][2] = %d\n", row[1][2]);
    printf("(*(row + 2))[0] = %d\n", (*(row + 2))[0]);

    print_matrix(3, 4, grid);

    return 0;
}

*row is a row of four integers, 16 bytes. row + 2 points to the third row, and (*(row + 2))[0] is its first element, written more simply as row[2][0]. print_matrix takes the column count first and uses it in the parameter type int (*m)[cols], a pointer to a variable-length array, so one function handles matrices of any width.

$ gcc -std=c17 -Wall -Wextra -o two_dim two_dim.c
$ ./two_dim
sizeof *row: 16
row[1][2] = 7
(*(row + 2))[0] = 9
  1  2  3  4
  5  6  7  8
  9 10 11 12

void pointers

A void * can hold the address of an object of any type, and C converts between void * and other object pointers without a cast. It cannot be dereferenced or used in arithmetic, because the size of the target is unknown; generic code passes the size separately and works on the bytes.

void_ptr.c
#include <stdio.h>
#include <string.h>

static void swap_any(void *a, void *b, size_t size) {

    unsigned char tmp[64];

    if (size > sizeof tmp) {
        return;
    }

    memcpy(tmp, a, size);
    memcpy(a, b, size);
    memcpy(b, tmp, size);
}

static void dump_bytes(const void *data, size_t size) {

    const unsigned char *p = data;

    for (size_t i = 0; i < size; i++) {
        printf("%02x ", p[i]);
    }

    printf("\n");
}

int main(void) {

    int i1 = 1, i2 = 2;
    double d1 = 1.5, d2 = -2.25;

    swap_any(&i1, &i2, sizeof i1);
    swap_any(&d1, &d2, sizeof d1);

    printf("i1 = %d, i2 = %d\n", i1, i2);
    printf("d1 = %.2f, d2 = %.2f\n", d1, d2);

    unsigned int v = 0x11223344;
    dump_bytes(&v, sizeof v);

    return 0;
}

swap_any exchanges two objects of any type through a temporary buffer of bytes, which is how qsort and similar library functions handle arbitrary elements. dump_bytes converts its void * to const unsigned char * to look at the bytes one by one. The value 0x11223344 is stored with its lowest byte first, which shows that x86-64 is a little-endian machine.

$ gcc -std=c17 -Wall -Wextra -o void_ptr void_ptr.c
$ ./void_ptr
i1 = 2, i2 = 1
d1 = -2.25, d2 = 1.50
44 33 22 11

Pointers to structures

Structures are usually passed to functions by pointer, which avoids copying them and allows the function to modify them. The -> operator accesses a member through a pointer: p->x means (*p).x.

struct_ptr.c
#include <stdio.h>

struct point {
    int x;
    int y;
};

static void move(struct point *p, int dx, int dy) {

    p->x += dx;
    p->y += dy;
}

int main(void) {

    struct point pts[] = { { 0, 0 }, { 3, 4 }, { -1, 2 } };
    size_t n = sizeof pts / sizeof pts[0];

    for (struct point *p = pts; p < pts + n; p++) {
        move(p, 10, 10);
    }

    for (size_t i = 0; i < n; i++) {
        printf("(%d, %d)\n", pts[i].x, (&pts[i])->y);
    }

    return 0;
}

The loop walks the array of structures with a pointer, exactly like an array of integers, and passes each element's address to move. The output loop shows that pts[i].x and (&pts[i])->y are two ways to reach a member. Structures are the subject of a separate tutorial.

$ gcc -std=c17 -Wall -Wextra -o struct_ptr struct_ptr.c
$ ./struct_ptr
(10, 10)
(13, 14)
(9, 12)

Dangling pointers

A pointer is only valid while the object it points to exists. A local variable ceases to exist when its function returns, so returning its address produces a dangling pointer.

dangling.c
#include <stdio.h>

static int *make_counter(void) {

    int counter = 0;

    return &counter;
}

int main(void) {

    int *c = make_counter();

    printf("%p\n", (void *) c);

    return 0;
}

GCC recognizes the mistake and warns. It also replaces the returned address with a null pointer, as the output shows, so the bug turns into a predictable crash at the first dereference instead of silent corruption; other compilers and less obvious cases are not so forgiving. The same problem arises with a pointer to memory that has been released with free.

$ gcc -std=c17 -Wall -Wextra -o dangling dangling.c
dangling.c: In function ‘make_counter’:
dangling.c:7:12: warning: function returns address of local variable [-Wreturn-local-addr]
    7 |     return &counter;
      |            ^~~~~~~~
$ ./dangling
(nil)

Returning valid pointers

There are three correct ways to give a caller a pointer that outlives the function: point to an object with static storage duration, allocate the object on the heap, or let the caller supply the memory.

lifetime.c
#include <stdio.h>
#include <stdlib.h>

static int *next_id(void) {

    static int id = 0;

    id++;

    return &id;
}

static int *new_value(int v) {

    int *p = malloc(sizeof *p);

    if (p != NULL) {
        *p = v;
    }

    return p;
}

static void fill(int *buf, size_t n) {

    for (size_t i = 0; i < n; i++) {
        buf[i] = (int) i + 1;
    }
}

int main(void) {

    next_id();
    int *id = next_id();
    printf("static: %d\n", *id);

    int *v = new_value(42);

    if (v == NULL) {
        return 1;
    }

    printf("heap: %d\n", *v);
    free(v);

    int buf[4];
    fill(buf, 4);
    printf("caller buffer: %d %d %d %d\n", buf[0], buf[1], buf[2], buf[3]);

    return 0;
}

The static variable in next_id exists for the whole run, so its address stays valid, although every call shares the same object. new_value returns heap memory, which the caller must free. The third form, where fill writes into a buffer owned by the caller, is the most common in the standard library, as in fgets and snprintf. Allocation is covered in C malloc and free.

$ gcc -std=c17 -Wall -Wextra -o lifetime lifetime.c
$ ./lifetime
static: 2
heap: 42
caller buffer: 1 2 3 4

Uninitialized pointers

A local pointer that is not initialized holds an arbitrary value, and writing through it can corrupt any part of memory. Unlike a null pointer, such a pointer cannot be tested.

uninit.c
#include <stdio.h>

int main(void) {

    int *p;
    int *q = NULL;

    if (q == NULL) {
        printf("q is NULL, not dereferenced\n");
    }

    *p = 5;
    printf("%d\n", *p);

    return 0;
}

With -Wall, GCC warns that p is used uninitialized. In this run the arbitrary address is not mapped and the program crashes, which is the lucky outcome. Initializing every pointer, with a valid address or with NULL, removes this whole class of bugs.

$ gcc -std=c17 -Wall -Wextra -o uninit uninit.c
uninit.c: In function ‘main’:
uninit.c:12:8: warning: ‘p’ is used uninitialized [-Wuninitialized]
   12 |     *p = 5;
      |     ~~~^~~
uninit.c:5:10: note: ‘p’ was declared here
    5 |     int *p;
      |          ^
$ ./uninit
q is NULL, not dereferenced
Segmentation fault (core dumped)

Out-of-bounds access

C does not check array indexes or pointer offsets. A loop that runs one step too far reads whatever lies after the array. The undefined behavior sanitizer, enabled with -fsanitize=undefined, adds checks for indexes into arrays whose size is known.

bounds.c
#include <stdio.h>

int main(void) {

    int vals[5] = { 1, 2, 3, 4, 5 };
    int sum = 0;

    for (int i = 0; i <= 5; i++) {
        sum += vals[i];
    }

    printf("sum = %d\n", sum);

    return 0;
}

The condition i <= 5 should be i < 5. The sanitizer reports the index of vals[5] and then the load from an address outside the array, both with the source line, and lets the program continue; the sum includes whatever value happened to follow the array, here 0. Without the sanitizer the program prints a result with no warning at all. AddressSanitizer, shown in C strings, catches the same error for heap memory and through pointers.

$ gcc -std=c17 -Wall -Wextra -fsanitize=undefined -o bounds bounds.c
$ ./bounds
bounds.c:9:20: runtime error: index 5 out of bounds for type 'int [5]'
bounds.c:9:20: runtime error: load of address 0x7fff5ba4d454 with insufficient space for an object of type 'int'
0x7fff5ba4d454: note: pointer points here
  05 00 00 00 00 00 00 00  05 00 00 00 0f 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
              ^
sum = 15

Guidelines

Pointer bugs are hard to find because the program often keeps running with corrupted data. A few habits prevent most of them.

Source

cppreference: pointer declaration
cppreference: member access operators
cppreference: array declaration
GCC: instrumentation options

This tutorial covered pointers in C: the address-of and dereference operators, passing pointers to functions, NULL, pointer arithmetic, the relation between arrays and pointers, const pointers, pointers to pointers and to arrays, void pointers, pointers to structures, object lifetime and the errors sanitizers detect. Strings, which are accessed almost entirely through pointers, are covered in C strings, arrays in C arrays, parameter passing in C functions, and heap memory in C malloc and free.

Author

My name is Jan Bodnar, and I'm a dedicated programmer with a deep passion for coding. Since 2007, I've been sharing my expertise through over 1,400 articles and 8 e-books. With more than a decade of teaching experience, I strive to make programming accessible and engaging.

List all C tutorials.