C Pointer

Although I haven’t usually used the pointers in recent years, sometimes I would be lost in C pointers when meeting some complex C pinter code.
Whenever I don’t understand, I need to search and learn the C pointers again. This time, I want to conclude C pointers and list the most common C pointer usages and mistakes we usually make.

Special Pointers NULL and void *.

In C language, we have two particular pointers, NULL and void *, the NULL means the pointer has not initialized. When you access a NULL pinter, there will be nothing to happen. Notice that the NULL must be capital. The null is a standard identifier in C.

In many standard libraries, they will check input pinter params. If their values are NULL, the function will prompt warnings or returns directly. So I suggest that we should also check input pinter params. Dong this can make our functions more robust.

When should we use the NULL pointer? The most common condition is when you declare a pinter now and assign a value to it later. If you declare a pointer like below, you would get an error when writing it or get a strange value when reading it.

1
2
3
4
5
6
7
8
int main() {
char *str;
/* The pinter str hasn't been initialized, its address might be any value.
If the address is unaccessible, you will get a crash. */
gets(str);
printf("%s", str);
return 0;
}

If you assign a NULL to the pointer str when declaring it, the printf will directly output (null) and return the function.

1
2
3
4
5
6
int main(){
char *str = NULL;
gets(str);
printf("%s", str);
return 0;
}

In fact, the NULL is a macro defined in stdio.h, its detailed define is #define NULL ((void *)0). The NULL points to a void point whose address is 0. The low part of the memory addresses is generally reserved, so the system can easily confirm whether the address is available.

The void is another particular keyword. A void * pinter is a valid pointer, but you don’t know its pinter type. The dynamic memory allocating function of C will return a void pointer. We need manually transform its type when using the pointer.

1
2
3
4
5
6
7
int main(){
// Allocate 10 bytes memory space and convert the return pointer to char *
char *str = (char *)malloc(sizeof(char) * 10);
gets(str);
printf("%s\n", str);
return 0;
}

Normal pinter & Array pointer

We all have learned the name of an array is the pointer that points to the beginning address of the array, we may think the pointer and the array are the same, but I’m afraid that’s not right. Arrays have their types

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(){
int a[6] = {0, 1, 2, 3, 4, 5};
int *p = a;
int len_a = sizeof(a) / sizeof(int);
int len_p = sizeof(p) / sizeof(int);
printf("len_a = %d, len_p = %d\n", len_a, len_p);
return 0;
}

// Output:
// len_a = 6, len_p = 1

An array contains a set of values, but it doesn’t have any end flag. When we set the a' to the p,thepis just anint pointer, its address point to the first item of the array. We use sizeofto get the size of the pointp` can only get the memory size of an int pointer. Wherever it points to, you can’t calculate the length of an array.

If the type of point p is int *, then the type of point a is int [6]. The int [6] is an array type, which means there are six integer values in a variable, so when we use sizeof to get the size of the pointer a, we get the result is the total size of six integers.

In some cases, An array type would be automatically converted to a normal pointer. For example, we read values through array subscript or pass an array to a function.

1
2
3
4
5
6
7
8
9
int main() {
int a[6] = {0, 1, 2, 3, 4, 5};

printf("result = %d", a[1]);
return 0;
}

// Output:
// result = 0

The a' in a[1]is a normal integer pointer. The compiler will convert the subscript to the expression*(a + 1), you can consider the []is an operator, its expression isx[y] = *(x + y), so you can rewrite the expression a[1]to1[a]`, they have the same effect, but the latter may be unreadable.

The other situation is you pass an array to a function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

void func1(int *arr) {
printf("%lu", sizeof(arr));
}
void func2(int arr[]) {
printf("%lu", sizeof(arr));
}
void func3(int arr[6]) {
printf("%lu", sizeof(arr));
}

int main() {
int a[6] = {0, 1, 2, 3, 4, 5};
func1(a);
func2(a);
func3(a);
return 0;
}

// Output(In 64 bits system):
// 888

Whatever type you define the param, you cannot get the length of the array. The compiler always passes a pointer, not copying the whole array to a function. An array can have a large number of items. If we copy its value when passing it to a function will waste a lot of memory. If you want to get the length of an array, you should pass the length through another param.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

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

int main() {
int a[6] = {0, 1, 2, 3, 4, 5};
func4(a, sizeof(a) / sizeof(int));
return 0;
}

Understand a complex pointer

Before analyzing a complex pointer, we review some common pointer types.

  • int *p: The p is a int pointer
  • int **p: The p is a pointer pointer, the p point to another pointer.
  • int p[n]: The p is an array, you can make it as a int pointer in calculation.
  • int *p[n]: The p is an array, The type of its items is int pointer.
  • int (*p)[n]: The p is a pointer, it point to an int array.
  • int (*p)(): The p is a pointer, it point to a function.

We also should know the precedence of the operators in pointer expression.

  • (): highest precedence
  • suffix [] and (): medium precedence (array and function)
  • prefix *: lowest precedence

In the pointer int *p[n], the precedence of [] if higher than the *, you can rewrite the expression to int *(p[n]), so we know the p is an array.

Practice

  • char *(* c[10])(int **p)

    1. Find the variable char *(* c[10])(int **p). We know the c is a pointer array. Its items are pointers.
    2. The suffix (int **p) means it’s a function. The function needs a pointer param pointing to an int array and returning a char pointer.
    3. Finally, we can get the c is a pointer array, its items are function pointers, the type of the function is char *f(int **p).
  • int (*(*(*p)(int *))[5])(int *)
    This expression is more complex, but it isn’t difficult for you if you can remember the rules above.

    1. Find the variable int (*(*(*p)(int *))[5])(int *). We know the p is a pointer.
    2. The suffix int (*(*(*p)(int *))[5])(int *) means the p is a function pinter. It needs an int pointer param and returns a pointer.
    3. What does the result pinter point to? int (*(*(*p)(int *))[5])(int *) It pointed to a pointer array, the length of the array is 5.
    4. The type of item in the pointer array is the function pointer. The function needs an int pointer param and returns an int value.

Conclusion

The pointer is the most crucial concept in C. It’s very flexible but always makes questions more complex. In fact, the basis of many languages is using pointers. Understanding pointers will help us to understand some data structures and algorithms further.