Mastering Pointers in C Language in Just 15 Minutes!

Mastering Pointers in C Language in Just 15 Minutes!

When it comes to pointers, many people might still feel confused, having a sense of “knowing it exists but not understanding how it works.” However, it must be said that learning pointers is essential for getting started with C language. Pointers are the essence of C language, and your proficiency with pointers directly determines your programming ability in C.

Before discussing pointers, let’s first understand how variables are stored in memory.

When you define a variable in a program, the system allocates corresponding memory space based on the type of variable during the compilation process. To use this variable, you simply need to access it by its name.

Accessing a variable by its name is a relatively safe method. Because only you defined it, you can access the corresponding variable. This is the basic understanding of memory. However, if you only know this, you still do not understand how memory actually stores variables, as you are unclear about how the underlying system works.

To delve deeper, you need to clarify what the actual representation of a variable in memory looks like. The smallest index unit of memory is 1 byte, so you can think of memory as a super large character array. As we discussed in the previous section, arrays have indices, and we access elements in an array using the array name and index. Memory works similarly; we just give it a new name: address. Each address can store 1 byte of data, so if we need to define an integer variable, it will occupy 4 memory units.

At this point, you might understand: during the execution of a program, there is no need for the variable name to be involved. The variable name is just a convenience for writing and reading code; only the programmer and the compiler know of its existence. The compiler also knows the specific memory address corresponding to the variable name, which we do not know. Therefore, the compiler acts as a bridge. When reading a variable, the compiler finds the address corresponding to the variable name and retrieves the corresponding value.

Introduction to Pointers and Pointer Variables

Now let’s get to the point: what exactly is a pointer?

A pointer is essentially a memory address (hereinafter referred to as address). In C language, there are special pointer variables designed to store pointers. Unlike ordinary variables, pointer variables store addresses.

Defining Pointers

Pointer variables also have types, which actually depend on the type of value the address points to. So how do you define a pointer variable?

It’s simple:<span><span>TypeName* PointerVariableName</span></span>

char* pa; // Define a pointer to a character variable named pa
int* pb; // Define a pointer to an integer variable named pb
float* pc; // Define a pointer to a float variable named pc

Note that the pointer variable must match the type of the variable it points to; otherwise, if the types are different, they may occupy different positions in memory, and defining it incorrectly may lead to errors.

Address Operator and Dereference Operator

To get the address of a variable, use the address operator<span><span>&</span></span>, for example:

char* pa = &a;
int* pb = &f;

If you want to access the data pointed to by the pointer variable, you need to use the dereference operator<span><span>*</span></span>, for example:

printf("%c, %d\n", *pa, *pb);

You might notice that the * symbol is also used when defining pointers; this is a case of symbol reuse. This means that the same symbol has different meanings in different contexts: when defining, it indicates defining a pointer variable, while at other times it is used to get the value pointed to by the pointer variable.

Accessing a variable’s value directly by its name is called direct access, while accessing it through a pointer is called indirect access, so the dereference operator is sometimes also referred to as the indirect operator.

For example:

//Example 01
// Code sourced from the internet, not original
#include 
int main(void)
{
    char a = 'f';
    int f = 123;
    char* pa = &a;
    int* pf = &f;
    
    printf("a = %c\n", *pa);
    printf("f = %d\n", *pf);
    
    *pa = 'c';
    *pf += 1;
    
    printf("now, a = %c\n", *pa);
    printf("now, f = %d\n", *pf);
    
    printf("sizeof pa = %d\n", sizeof(pa));
    printf("sizeof pf = %d\n", sizeof(pf));
    
    printf("the addr of a is: %p\n", pa);
    printf("the addr of f is: %p\n", pf);
    
    return 0;
}

The program implementation is as follows:

//Consequence 01
a = f
f = 123
now, a = c
now, f = 124
sizeof pa = 4
sizeof pf = 4
the addr of a is: 00EFF97F
the addr of f is: 00EFF970

Avoid Accessing Uninitialized Pointers

void f()
{
    int* a;
    *a = 10;
}

Code like this is very dangerous. Because we do not know where pointer a points to. It is similar to accessing an uninitialized ordinary variable, which will return a random value. However, if it is in a pointer, it may overwrite other memory areas, and it could even be a critical area that the system is currently using, which is very dangerous. However, in such cases, the system usually rejects the program’s execution, and the program will be terminated and report an error. If by chance it hits a valid address, the subsequent assignment may lead to some useful data being mysteriously modified, making such bugs very difficult to trace. Therefore, when using pointers, it is essential to pay attention to initialization.

Pointers and Arrays

Some readers might wonder, what is the relationship between pointers and arrays? These two seem to be unrelated. Don’t worry, keep reading, your perspective may change.

Address of an Array

We just mentioned that pointers are essentially the addresses of variables in memory. So if you have an array, which is a collection of many variables, what is its address?

We know that to read a value from the standard input stream into a variable, we use the scanf function, and generally, we need to add & at the end. This is actually the address operator we just mentioned. If the storage location is a pointer variable, then it is not needed.

//Example 02
int main(void)
{
    int a;
    int* p = &a;
    
    printf("Please enter an integer:");
    scanf("%d", &a); // Here & is needed
    printf("a = %d\n", a);
    
    printf("Please enter another integer:");
    scanf("%d", p); // Here & is not needed
    printf("a = %d\n", a);
    
    return 0;
}

The program runs as follows:

//Consequence 02
Please enter an integer: 1
a = 1
Please enter another integer: 2
a = 2

When reading a normal variable, the program needs to know the address of that variable in memory, so it requires & to complete this task. For pointer variables, it is already another ordinary variable’s address information, so you can directly provide the pointer’s value.

Consider when we use the scanf function, there are times when we do not need to use &? This is when reading strings:

//Example 03
#include 
int main(void)
{
    char url[100];
    url[99] = '\0';
    printf("Please enter the domain name of TechZone:");
    scanf("%s", url); // Here & is also not needed
    printf("The domain name you entered is: %s\n", url);
    return 0;
}

The program executes as follows:

//Consequence 03
Please enter the domain name of TechZone: www.techzone.ltd
The domain name you entered is: www.techzone.ltd

Therefore, it is easy to deduce: the array name is actually an address information, which is essentially the address of the first element of the array. Let’s try comparing the address of the first element with the address of the array:

//Example 03 V2
#include 
int main(void)
{
    char url[100];
    printf("Please enter the domain name of TechZone:");
    url[99] = '\0';
    scanf("%s", url);
    printf("The domain name you entered is: %s\n", url);

    printf("The address of url is: %p\n", url);
    printf("The address of url[0] is: %p\n", &url[0]);

    if (url == &url[0])
    {
        printf("Both are the same!");
    }
    else
    {
        printf("Both are not the same!");
    }
    return 0;
}

The program runs as follows:

//Consequence 03 V2
Please enter the domain name of TechZone: www.techzone.ltd
The domain name you entered is: www.techzone.ltd
The address of url is: 0063F804
The address of url[0] is: 0063F804
Both are the same!

From this perspective, it seems confirmed. The subsequent elements of the array are stored sequentially, and those interested can write code to try outputting them.

Pointers to Arrays

We have just verified that the address of an array is the address of its first element. Therefore, there are two ways to define a pointer to an array:

...
char* p;
// Method 1
p = a;
// Method 2
p = &a[0];

Pointer Arithmetic

When a pointer points to an array element, you can perform addition and subtraction on the pointer variable. +n indicates pointing to the next n elements of the element pointed to by pointer p, while -n indicates pointing to the previous n elements. It does not simply add 1 to the address.

For example:

//Example 04
#include 
int main(void)
{
    int a[] = { 1, 2, 3, 4, 5 };
    int* p = a;
    printf("*p = %d, *(p+1) = %d, *(p+2) = %d\n", *p, *(p + 1), *(p + 2));
    printf("*p -> %p, *(p+1) -> %p, *(p+2) -> %p\n", p, p + 1, p + 2);
    return 0;
}

The execution result is as follows:

//Consequence 04
*p = 1, *(p+1) = 2, *(p+2) = 3
*p -> 00AFF838, *(p+1) -> 00AFF83C, *(p+2) -> 00AFF840

Some may wonder how the compiler knows to access the next element instead of just adding 1 to the address.

Actually, when we define the pointer variable, we have already informed the compiler. If we define a pointer to an integer array, then when the pointer is incremented, it actually adds a distance of sizeof(int). Compared to standard subscript access, using pointers to indirectly access array elements is called pointer method.

In fact, using the pointer method to access array elements does not necessarily require defining a separate pointer variable pointing to the array, because the array name itself is a pointer to the first element of the array. Therefore, the pointer method can directly apply to the array name:

...
printf("p -> %p, p+1 -> %p, p+2 -> %p\n", a, a+1, a+2);
printf("a = %d, a+1 = %d, a+2 = %d", *a, *(a+1), *(a+2));
...

The execution result is as follows:

p -> 00AFF838, p+1 -> 00AFF83C, p+2 -> 00AFF840
b = 1, b+1 = 2, b+2 = 3

Now you might feel that arrays and pointers are somewhat similar, but let me remind you that although arrays and pointers are very similar, they are definitely not the same thing.

You can even directly use pointers to define strings and then use subscript method to read each character:

//Example 05
// Code sourced from the internet
#include 
#include 
int main(void)
{
    char* str = "I love TechZone!";
    int i, length;
    
    length = strlen(str);
    
    for (i = 0; i < length; i++)
    {
        printf("%c", str[i]);
    }
    printf("\n");
    
    return 0;
}

The program runs as follows:

//Consequence 05
I love TechZone!

In the previous code, we defined a character pointer variable and initialized it to point to a string. The subsequent operations can not only use string processing functions on it but also use subscript method to access each character in the string.

Of course, the loop can also be written like this:

...
for (i = 0; i < length; i++)
{
    printf("%c", *(str + i));
}

This effectively utilizes the pointer method to read.

Differences Between Pointers and Arrays

Having discussed many examples of pointers and arrays being interchangeable, some may start to think: “Aren’t these two just the same thing?”

As you become more familiar with pointers and arrays, you will realize that the creators of C language would not have created two identical things and given them different names. Pointers and arrays are ultimately different.

For example, I once saw an example:

//Example 06
// Code sourced from the internet
#include 
int main(void)
{
    char str[] = "I love TechZone!";
    int count = 0;
    
    while (*str++ != '\0')
    {
        count++;
    }
    printf("There are a total of %d characters.\n", count);
    
    return 0;
}

When the compiler reports an error, you might start to doubt that you learned the wrong C language syntax:

//Error in Example 06
Error (active) E0137 Expression must be a modifiable lvalue
Error C2105 "++" requires an lvalue

We know that *str++ != '\0' is a compound expression, so we need to follow the operator precedence. You can refer to the “C Language Operator Precedence and ASCII Reference Table” for details.

<span><span>str++</span></span> has a higher precedence than *str, but the increment operator only takes effect in the next statement. So the understanding of this statement is that it first retrieves the value pointed to by str, checks if it is \0, and if so, exits the loop, then str points to the next character’s position.

It seems there is nothing wrong, but look at what the compiler tells us:

<span><span>Expression must be a modifiable lvalue</span></span>

<span><span>++</span></span> operates on str, so is str a modifiable lvalue?

If it is a modifiable lvalue, it must meet the conditions of a modifiable lvalue.

  1. Has an identifier used to identify and locate a storage location

  2. Value stored can be modified

The first point, the array name str can satisfy this because the array name actually locates the position of the first element of the array. However, the second point does not satisfy because the array name is actually an address, and addresses are not modifiable; they are constants. If you want to implement the above logic, you can change the code like this:

//Example 06 V2
// Code sourced from the internet
#include 
int main(void)
{
    char str[] = "I love TechZone!";
    char* target = str;
    int count = 0;
    
    while (*target++ != '\0')
    {
        count++;
    }
    printf("There are a total of %d characters.\n", count);
    
    return 0;
}

This can execute normally:

//Consequence 06 V2
There are a total of 16 characters.

Thus, we can conclude that the array name is merely an address, while a pointer is a modifiable lvalue.

Pointer Arrays? Array Pointers?

Look at the following example, can you distinguish which is a pointer array and which is an array pointer?

int* p1[5];
int(*p2)[5];

We can judge each one individually, but it becomes a bit tricky when combined.

The answer:

int* p1[5]; // Pointer array
int(*p2)[5]; // Array pointer

Let’s analyze them one by one.

Pointer Array

The array subscript [] has the highest precedence, so p1 is an array with 5 elements. What is the type of this array? The answer is int*, which is a pointer to an integer variable. Therefore, this is a pointer array.

So how should such an array be initialized?

You can define 5 variables and initialize them one by one by taking their addresses.

However, this is too cumbersome, but it does not mean that pointer arrays are useless.

For example:

//Example 07
#include 
int main(void)
{
    char* p1[5] = {
        "Life is short, I use Python.",
        "PHP is the best language in the world!",
        "One more thing...",
        "A good programmer should be the kind of person who looks both ways before crossing a single line.",
        "C language can easily lead you to make mistakes; C++ seems better, but when you use it, you will find it dies even worse."
    };
    int i;
    for (i = 0; i < 5; i++)
    {
        printf("%s\n", p1[i]);
    }
    return 0;
}

The result is as follows:

//Consequence 07
Life is short, I use Python.
PHP is the best language in the world!
One more thing...
A good programmer should be the kind of person who looks both ways before crossing a single line.
C language can easily lead you to make mistakes; C++ seems better, but when you use it, you will find it dies even worse.

This is much more straightforward and easier to understand than a two-dimensional array, right?

Array Pointer

<span><span>()</span></span> and [] have the same precedence, so we proceed according to order of precedence.

<span><span>int(*p2)</span></span> defines p2 as a pointer, followed by an array of 5 elements, so p2 points to this array. Therefore, an array pointer is a pointer that points to an array.

However, be careful when initializing an array pointer. For example:

//Example 08
#include 
int main(void)
{
    int(*p2)[5] = {1, 2, 3, 4, 5};
    int i;
    for (i = 0; i < 5; i++)
    {
        printf("%d\n", *(p2 + i));
    }
    return 0;
}

Visual Studio 2019 reports the following errors:

//Error and Warning in Example 08
Error (active) E0146 Initializer element is too large
Error C2440 "initialization": cannot convert from "initializer list" to "int (*)[5]"
Warning C4477 "printf": format string "%d" requires a matching argument of type "int", but the variable argument 1 has type "int *"

This is a very typical case of incorrect pointer usage. The compiler indicates that there is an issue with assigning an integer to a pointer variable because p2 is ultimately still a pointer, so it should be passed an address. Let’s modify it:

//Example 08 V2
#include 
int main(void)
{
    int temp[5] = {1, 2, 3, 4, 5};
    int(*p2)[5] = temp;
    int i;
    for (i = 0; i < 5; i++)
    {
        printf("%d\n", *(p2 + i));
    }
    return 0;
}
//Error and Warning in Example 08 V2
Error (active) E0144 "int *" type value cannot be used to initialize "int (*)[5]" type entity
Error C2440 "initialization": cannot convert from "int [5]" to "int (*)[5]"
Warning C4477 "printf": format string "%d" requires a matching argument of type "int", but the variable argument 1 has type "int *"

But why is there still a problem?

Let’s review how pointers point to arrays.

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

We originally thought that pointer p points to the array, but in fact, it does not. Upon careful consideration, we find that this pointer actually points to the first element of the array, not the array itself. Since the elements of the array are stored contiguously in memory, knowing the address of the first element allows access to all subsequent elements.

However, from this perspective, pointer p points to an integer variable, not to an array. The array pointer we just used is the one that points to the array. Therefore, we should pass the address of the array to the array pointer, not the address of the first element, even though they have the same value, their meanings are indeed different:

//Example 08 V3
//Example 08 V2
#include 
int main(void)
{
    int temp[5] = {1, 2, 3, 4, 5};
    int(*p2)[5] = &temp; // Here we take the address
    int i;
    for (i = 0; i < 5; i++)
    {
        printf("%d\n", *(*p2 + i));
    }
    return 0;
}

The program runs as follows:

//Consequence 08
1
2
3
4
5

Pointers and Two-Dimensional Arrays

In the previous section, we discussed the concept of two-dimensional arrays, and we also know that two-dimensional arrays in C language are actually linearly stored in memory.

Assuming we define:<span><span>int array[4][5]</span></span>

Array

The name array should represent the base address of the array. Since a two-dimensional array is essentially a linear extension of a one-dimensional array, array should point to a pointer to an array containing 5 elements.

If you use sizeof() to test array and array+1, you can derive this conclusion.

*(array+1)

From the previous question, we can conclude that array+1 also points to a pointer to an array containing 5 elements, so *(array+1) is equivalent to array[1], which is exactly the same as array[1][0] in terms of the array name. Therefore, *(array+1) points to the address of the first element of the second row sub-array.

*(*(array+1)+2)

With the previous conclusion, we can easily deduce that this is actually array[1][2]. Doesn’t it seem very simple?

To summarize, here are some conclusions to remember, understanding them is even better:

*(array + i) == array[i]
*(*(array + i) + j) == array[i][j]
*(*(*(array + i) + j) + k) == array[i][j][k]
...

Array Pointers and Two-Dimensional Arrays

In the previous section, we mentioned that when initializing a two-dimensional array, you can be a bit lazy:

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

We just said that defining an array pointer is like this:

int(*p)[3];

So what does combining them mean?

int(*p)[3] = array;

From the previous explanation, we know that array is a pointer to an array of 3 elements, so we can completely assign the value of array to p.

In fact, pointers in C language are very flexible; the same code can be interpreted differently for various applications.

So how do we use pointers to access two-dimensional arrays? That’s right, by using array pointers:

//Example 09
#include 
int main(void)
{
    int array[3][4] = {
        {0, 1, 2, 3},
        {4, 5, 6, 7},
        {8, 9, 10, 11}
    };
    int(*p)[4];
    int i, j;
    p = array;
    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 4; j++)
        {
            printf("%2d ", *(*(p+i) + j)); 
        }
        printf("\n");
    }
    return 0;
}

The execution result is:

//Consequence 09
 0  1  2  3
 4  5  6  7
 8  9 10 11

Void Pointers

<span><span>void</span></span> actually means no type. If you try to use it to define a variable, the compiler will definitely report an error because different types may occupy different memory sizes. However, if you define a pointer, it is fine.<span><span>void</span></span> pointers can point to any type of data, meaning any type of pointer can be assigned to a void pointer.

Converting any type of pointer to void is not a problem. However, if you want to convert back, you need to perform type casting. Additionally, do not dereference a void pointer directly, as the compiler does not know what type of data the void pointer will store.

//Example 10
#include 
int main(void)
{
    int num = 1024;
    int* pi = &num;
    char* ps = "TechZone";
    void* pv;
    
    pv = pi;
    printf("pi:%p,pv:%p\n", pi, pv);
    printf("*pv:%d\n", *pv);
    
    pv = ps;
    printf("ps:%p,pv:%p\n", ps, pv);
    printf("*pv:%s\n", *pv);
}

This will report an error:

//Error in Example 10
Error C2100 Illegal indirect addressing
Error C2100 Illegal indirect addressing

If you must do this, you can use type casting:

//Example 10 V2
#include 
int main(void)
{
    int num = 1024;
    int* pi = &num;
    char* ps = "TechZone";
    void* pv;

    pv = pi;
    printf("pi:%p,pv:%p\n", pi, pv);
    printf("*pv:%d\n", *(int*)pv);

    pv = ps;
    printf("ps:%p,pv:%p\n", ps, pv);
    printf("*pv:%s\n", (char*)pv);
}

Of course, using void pointers should be done with caution, as they can almost accept all types, making it easy to perform illegal conversions that the compiler will not catch.

Therefore, it is recommended to avoid using void pointers unless necessary; we will unlock more new functionalities when discussing functions later.

NULL Pointers

In C language, if a pointer does not point to any data, it is called a NULL pointer, represented by NULL. NULL is actually a macro definition:

#define NULL ((void *)0)

In most operating systems, address 0 is usually a not used address, so if a pointer points to NULL, it means it does not point to anything. Why should a pointer point to NULL?

Actually, this is a recommended programming style—when you temporarily do not know where to point, let it point to NULL to avoid many troubles. For example:

//Example 11
#include 
int main(void)
{
    int* p1;
    int* p2 = NULL;
    printf("%d\n", *p1);
    printf("%d\n", *p2);
    return 0;
}

The first pointer is uninitialized. In some compilers, such uninitialized variables will be assigned a random value. This type of pointer is called a dangling pointer, wild pointer, or invalid pointer. If subsequent code dereferences this type of pointer and the address happens to be valid, it can lead to inexplicable results or even cause the program to crash. Therefore, developing a good habit of using NULL when unsure can save a lot of debugging time later.

Pointers to Pointers

Now we are getting into more complex territory. As long as you understand the concept of pointers, it is not a big deal.

//Example 12
#include 
int main(void)
{
    int num = 1;
    int* p = &num;
    int** pp = &p;
    
    printf("num: %d\n", num);
    printf("*p: %d\n", *p);
    printf("**pp: %d\n", **pp);
    printf("&p: %p, pp: %p\n", &p, pp);
    printf("&num: %p, p: %p, *pp: %p\n", &num, p, *pp);
    return 0;
}

The program result is as follows:

//Consequence 12
num: 1
*p: 1
**pp: 1
&p: 004FF960, pp: 004FF960
#: 004FF96C, p: 004FF96C, *pp: 004FF96C

Of course, you can keep nesting pointers indefinitely, but this will make the code readability very poor, and after a while, you might not even understand the code you wrote.

Pointer Arrays and Pointers to Pointers

So, what is the use of pointers to pointers? It is not to create chaotic code; you can experience its usefulness in a classic example:

char* Books[] = {
    "C Programming Expert",
    "C and Pointers",
    "C Traps and Pitfalls",
    "C Primer Plus",
    "Python Basics (3rd Edition)"
};

Then we need to categorize these books. We find that one of them is about Python, while the others are about C language. This is where pointers to pointers come in handy. First, we just defined a pointer array, meaning all elements in it are pointers, and the array name can also be accessed in the form of a pointer. Therefore, we can use a pointer to pointer to point to the pointer array:

...
char** Python;
char** CLang[4];

Python = &Books[5];
CLang[0] = &Books[0];
CLang[1] = &Books[1];
CLang[2] = &Books[2];
CLang[3] = &Books[3];
...

Since the address of a string is actually its base address, which is a pointer to a character pointer, we can assign it this way.

Thus, we have categorized the books using pointers to pointers, which not only avoids wasting extra memory but also allows for easy modification of book titles, enhancing the flexibility and safety of the code.

Constants and Pointers

Constants, in our current understanding, should be like this:

520, 'a'

Or like this:

#define MAX 1000
#define B 'b'

The biggest difference between constants and variables is that the former cannot be modified, while the latter can. In C language, you can make a variable behave like a constant by using const.

const int max = 1000;
const char a = 'a';

Under the effect of the const keyword, the variable will lose its original modifiable property and become “read-only”.

Pointers to Constants

Powerful pointers can also point to variables modified by const, but this means that you cannot modify the value it references through the pointer. To summarize, here are 4 points:

  1. Pointers can be modified to point to different variables

  2. Pointers can be modified to point to different constants

  3. You can read the data pointed to by dereferencing the pointer

  4. You cannot modify the data pointed to by dereferencing the pointer

Constant Pointers

Constant Pointers to Non-Constants

Since pointers are a type of variable, they can also be modified. Therefore, pointers can be modified by const, but the position is slightly changed:

...
int* const p = &num;
...

This type of pointer has the following characteristics:

  1. The pointer itself cannot be modified

  2. The value pointed to by the pointer can be modified

Constant Pointers to Constants

When defining ordinary variables, using const to modify them results in such pointers. However, due to too many restrictions, they are generally rarely used:

...
int num = 100;
const int cnum = 200;
const int* const p = &cnum;
...

Mastering Pointers in C Language in Just 15 Minutes!

Mastering Pointers in C Language in Just 15 Minutes!

Some screenshots of electronic books

Mastering Pointers in C Language in Just 15 Minutes!

Leave a Comment