When it comes to Pointers, it’s impossible to get away from memory, and people who learn Pointers are divided into those who don’t understand the memory model and those who do.

Do not understand the understanding of Pointers to stay in the “pointer is the address of the variable” this sentence, will be more afraid to use Pointers, especially a variety of advanced operations.

And understand the memory model can use the pointer to perfect, various byte arbitrary operation, let a person call 666.

After reading this article, I believe you will have a new understanding of Pointers ~

NO.1

Nature of memory

The essence of programming is manipulating data, which is stored in memory.

Therefore, a better understanding of the memory model, and how C manages memory, gives you insight into how your programs work and improves your programming ability to the next level.

You really don’t think this is empty talk, I didn’t dare to write thousands of lines of programs in C all freshman year and I really resisted writing in C.

Because once thousands of lines, often appear all kinds of inexplicable memory errors, accidentally occurred coredump…… And there’s no way to find out why.

In contrast, I liked Java the most at that time. No NullPointerException would occur in Java. At most, NullPointerException would be found occasionally.

It wasn’t until I had a better understanding of memory and Pointers that I started writing thousands of lines of projects in C and rarely had memory problems.

Pointers store the memory address of variables. This sentence should be mentioned in any C language book.

So, to fully understand Pointers, you first need to understand the nature of variable storage in C, which is memory.

1.1 Memory addressing

A computer’s memory is a block of space used to store data, consisting of a series of contiguous storage units, like the one below

Each cell represents a Bit. In the eyes of EE students, a Bit is high or low potential, while in the eyes of CS students, it is 0 or 1.

Since one bit can only represent two states, the big guys specify a group of eight bits named byte.

In addition, byte is regarded as the smallest unit of memory addressing, that is, each byte is given a number, which is called the address of memory.

This is equivalent to assigning a number to each unit and household in the community: 301, 302, 403, 404, 501……

In life, we need to ensure that the house number is unique, so that we can accurately locate the family through the house number.

Similarly, in a computer, we need to ensure that each byte number is unique to ensure that each byte number has access to a unique, identifiable byte.

1.2 Memory Address space

Given a unique number for each byte in memory, the range of the number determines the size of the computer’s addressable memory.

All the numbers together are called the address space of memory, depending on whether the computer is 32-bit or 64-bit.

Early Intel 8086 and 8088 cpus only supported 16-bit address space, registers and address bus were 16-bit, which meant addressing memory numbers up to 2^16 = 64 Kb.

This memory space is obviously not enough, later, the 80286 will be based on the 8086 address bus and address register extended to 20 bits, also known as A20 address bus.

When writing mini OS at that time, also need to start the A20 address bus switch through BIOS interrupt.

However, today’s computers typically start with 32 bits, which means the addressable memory range is 2^32 bytes = 4GB.

So, if you have a 32-bit computer, you won’t be able to use up more than 4 gigabytes of memory.

Ok, so that’s memory and memory addressing.

1.3 Nature of variables

Now that we have memory, we need to think about how variables like int and double are stored in cells 0 and 1.

In C we define variables like this:

When you write down a variable definition, you are actually asking memory for space to hold your variable.

We all know that ints take up four bytes, and that in computers numbers are represented by a complement.

If you convert 999 into a complement, it is 0000 0011 1110 0111

There are four bytes, so four cells are needed to store:

Notice that we put the higher byte in the lower address.

Could it be the other way around?

This, of course, brings us to the big end and the little end.

The way we put the most important byte in the lower memory address is called the big endian, and the way we put the least important byte in the lower memory address is called the little endian.

The above shows how variables of type int are stored in memory, but float, char, etc., are also converted to complement first.

For multi-byte variable types, bytes are also written to memory cells in either big-endian or small-endian format.

Remember these two pictures, this is what all variables in a programming language look like in memory, be they ints, chars, Pointers, arrays, structures, objects… It’s all in memory like this.

NO.2

What is a pointer? 2.1 Where do variables go?

I said that defining a variable is actually asking the computer for a block of memory to hold it.

So what if we want to know where the variables are?

The actual address of the variable can be obtained by using the & operator. This value is the starting address of the block of memory occupied by the variable. (PS: actually this address is a virtual address, not a real physical memory address)

We can print out this address:

It would look something like this: 0x7FFcad3b8f3c

2.2 Essence of Pointers

We can use the ampersand symbol to retrieve the memory address of a variable. How can we use the ampersand symbol to indicate that this is an address and not an ordinary value?

How do you represent an address in C?

Yeah, it’s a pointer. You can do this:

Pa stores the address of variable A, also known as a pointer to A.

Here I’d like to talk about a few topics that seem a bit boring:

“Why do we need Pointers? Can’t you just use the variable name?”

– Sure, but variable names are limited.

“What is the nature of a variable name?”

– is the symbol of the address of the variable, the variable is to make our programming more convenient, user-friendly, but the computer does not know what variable A, it only knows the address and instructions.

So when you look at the compiled assembly code in C, the variable names disappear, replaced by a string of abstract addresses.

You can assume that the compiler automatically maintains a map that translates the names of variables in our program to the address they correspond to, and then reads and writes to that address.

That is, there is a mapping table that automatically converts variable names to addresses:

Say of good!

But I still don’t know the need for Pointers, so the problem is, look at the following code:

Suppose I have a requirement:

The func function is required to be able to modify the main variable a, which can read and write a directly from the main variable name.

But you can’t see a in func.

You can pass in the address of A by taking the address symbol ampersand:

So func can get the address of a, read and write.

That’s fine in theory, but here’s the thing:

How does the compiler distinguish between an int and the address (pointer) of another variable?

If this were left entirely to us programmers to remember, it would introduce complexity and make it impossible for the compiler to detect syntax errors.

Using int * to define a pointer variable is very clear: this is the address of another int variable.

The compiler can also rule out some compilation errors through type checking.

That’s why Pointers exist.

Virtually any language has this requirement, but many languages wrap Pointers as references for security purposes.

Perhaps everyone is learning to accept the pointer this thing naturally, but I still hope that this long-winded explanation to you have some inspiration.

In the meantime, here’s a quick question:

Since Pointers are essentially the memory header of a variable, that is, an integer of type int.

– So why are there different types?

– Such as int Pointers, float Pointers, does this type affect the information stored in the pointer itself?

– When will this type come into play?

Reference 2.3 solution

The above question is designed to elicit pointer dereference.

Pa stores the memory address of variable A, so how to obtain the value of A through the address?

This operation is called dereference, and in C we use the * operator to retrieve the contents of a pointer to the address.

Pa, for example, will give you the value of A.

We say that Pointers store the beginning address of variable memory, so how does the compiler know how many bytes to fetch from the beginning address?

This is where pointer types come into play, and the compiler determines how many bytes to fetch based on the type of the element to which the pointer refers.

If it is an int, the compiler generates instructions to extract four bytes, char extracts only one byte, and so on.

Here is a sketch of pointer memory:

The pa pointer is first and foremost a variable, and it occupies a chunk of memory that contains the starting address of variable A.

When dereferencing, four consecutive bytes are delimited from the header and interpreted as ints.

2.4 Live learning and application

It’s not that simple, but it’s the key to understanding Pointers.

Here are two examples to illustrate:

Such as:

Can you explain what happens at the memory level with respect to the f variable?

Or what is the value of c? 1?

In fact, f doesn’t change anything at the memory level.

As shown in figure:

Assuming that this is the bit pattern of F in memory, the process essentially involves taking the first two bytes of F and interpreting them as short, and assigning them to C.

The detailed process is as follows:

  1. &f gets the first address of f

  2. (short*)&f

The second step above does nothing, this expression just says:

“Oh, I think the address f is a short variable.”

Finally, when dereferencing (short)&f, the compiler takes the first two bytes, interprets them as short, and assigns the interpreted value to the C variable.

Nothing changes in the bit pattern of f, only in the way the bits are interpreted.

Of course, the final value here is definitely not 1, but you can actually calculate what it is.

How about the other way around? How about this?

As shown in figure:

The exact process is the same as above, but you can’t get an error up here, not here.

Why is that?

(float*)&c will let us take four bytes from the beginning of C and interpret them as float is encoded.

But c is short and only takes two bytes, so it must access the next two bytes, and that’s when memory access is out of bounds.

Of course, if you just read, it’s probably fine.

However, sometimes you need to write a new value to this field, for example:

Then coredUMP, or fetch failure, may occur.

In addition, even without coredump, this will destroy the original value of this block of memory, because it is likely that this is the memory space of other variables, and we overwrite the contents of others, which will definitely lead to hidden bugs.

Once you understand the above, you’ll be much more comfortable with Pointers.

2.6 Look at a small problem

At this point, let’s look at a question, this is from a group member, this is his demand:

Here’s the code he wrote:

He writes the double into the file and reads it out, and then realizes that the printed values don’t match.

And here’s the key:

He might think that a buffer is a pointer (an array, to be exact), and that dereferencing a pointer should take the values inside, which he thinks are four bytes read from a file, the former float variable.

Notice that this is all he thinks, but the compiler actually thinks:

Buffer is a pointer to a char, so I’ll take the first byte.

It then passes the value of the first byte to the printf function, which finds that %f is required to receive a float and automatically converts the value of the first byte to a float and prints it.

That’s the whole process.

The key error is that the student mistakenly thinks that any pointer dereference is getting “the value we think it is” inside, but the compiler doesn’t know that, it just foolishly interprets the pointer type.

So let’s change this to:

It explicitly tells the compiler:

“The buffer points to a place where I put a float. Please explain it to me as float.”

NO.3

Structure and pointer

The body of a structure contains multiple members. How are these members stored in memory?

Such as:

This is a fixed-point decimal structure that takes up 8 bytes in memory (memory alignment is not considered here). The two member fields are stored like this:

We put 10 in the structure with the base address offset of 0, and 2 in the structure with the base address offset of 4.

Now let’s do something that a normal person would never do:

How much is this going to output? Think about it for yourself

Here’s what happens:

First, &fp.denom means taking the first address of the denom field in the structure FP, and then taking 8 bytes from that address, and treating them as a fraction structure.

In this new structure, the top four bytes become the denOM field, and the DENOM field of FP corresponds to the NUM field of the new structure.

Therefore:

((fraction*)(&fp.denom))->num = 5

What actually changed was FP.denom, and

((fraction*)(&fp.denom))->denom = 12

The top four bytes are assigned 12.

Of course, writing to that four bytes of memory is unpredictable and can cause a program to crash, either because it happens to hold key information about the function call stack frame, or because there is no write permission there.

A lot of coredump errors that you’re starting out with in C are due to similar reasons.

So the final output is 5.

Why are we talking about code that doesn’t make any sense?

Just to show you that the essence of a structure is just a bunch of variables packed together, and to access a field in a structure, you go to the starting address of the structure, which is called the base address, and then you add the offset of the field.

In fact, objects in C++ and Java are also stored in this way, except that they will add some Head information in addition to data members in order to achieve some object-oriented features, such as C++ virtual function table.

In fact, we can use C language to imitate.

That’s why it’s always said that C is the foundation, that you really understand C Pointers and memory, and that you’ll quickly understand object models and memory layouts in other languages.

NO.4

Multilevel pointer

Speaking of multi-level Pointers, I used to be a freshman, and at most I could understand level 2. No matter how much it was, it would really confuse me and often make mistakes in code.

Int ******p can break me up. I think many students are in the same situation now

In fact, multi-level Pointers are not so complex, is the pointer of the pointer of the pointer of the pointer…… Very simple.

Today I will take you to understand the nature of multilevel Pointers.

First of all, let me say that there is no such thing as multilevel Pointers, Pointers are Pointers, and multilevel Pointers are just logical concepts for our convenience.

First take a look at the delivery cabinet in life:

I think you’ve all used this before, like a nest or a supermarket locker, where each box has a number, and we just have to get the number, and then we can find the corresponding box and take out what’s in it.

The cells here are the memory cells, the numbers are the addresses, and the things in the cells correspond to what’s stored in memory.

Suppose I put a book on grid 03, and I give you the number 03, and you can pick up the book according to 03.

What if I put the book on grid 05, and on grid 03 I just put a little note that says, “Book on grid 05.”

What would you do?

Of course is to open the 03 grid, and then took out the slip, according to the above content to open 05 grid to get the book.

Box 03 here is called a pointer because it contains little pieces of paper (addresses) pointing to other boxes rather than specific books.

Is that clear?

What if I put the book on grid 07 and put a note on grid 05 saying “Book on grid 07” and a note on grid 03 saying “Book on grid 05”?

Here, cell 03 is called a second level pointer, cell 05 is called a pointer, and cell 07 is our usual variable.

In turn, n-class Pointers can be derived by class.

So do you get it? The same piece of memory, if the storage is the address of other variables, so called Pointers, storage is the actual content, called variables.

Above this code, PA is called a pointer, that is, usually often said pointer, PPA is the second pointer.

Memory diagram is as follows:

No matter what level of pointer there are two core things:

– The pointer itself is also a variable that requires memory to store, and the pointer also has its own address

– Pointer memory stores the address of the variable it points to

That’s why I think multilevel Pointers are a logical concept. In fact, a block of memory can either hold the actual content or some other variable address. It’s as simple as that.

How to interpret the expression int **a?

Int ** a (a); int* * a (a); int* * a (a)

Only addresses of int* variables can be stored.

For a second-level pointer or even a multilevel pointer, we can split it into two parts.

First of all, no matter how many levels of pointer variable, it is a pointer variable first, the pointer variable is a *, the rest of the * represents the pointer variable can only store the address of the type of variable.

For example, int**a indicates that pointer variable A can hold only the addresses of int* variables.

NO.5

Pointers and Arrays

5.1 One-dimensional Array

Arrays are basic data structures that come with C, and a thorough understanding of arrays and their usage is fundamental to developing efficient applications.

Array and pointer representations are closely related and interchangeable when appropriate.

As follows:

In memory, an array is a contiguous chunk of memory:

The address of the 0th element is called the first address of the array. The name of the array actually refers to the first address of the array when we access the element through array[1] or *(array + 1).

Address [offset] = address[offset] = offset; address[offset] = address[offset] = offset; Address + sizeof(int) * offset.

For those of you who have studied assembly, you must be familiar with this approach, which is one of the addressing methods in assembly: base address to address.

After reading the above code, many students might think that Pointers and arrays are exactly the same and interchangeable, which is completely wrong.

Array names are not Pointers, although they can sometimes be used as Pointers.

The most typical place is sizeof:

The first will print 40, since the array contains 10 ints, and the second on a 32-bit machine will print 4, which is the length of the pointer.

Why is that?

From a compiler’s point of view, variable names and array names are symbols, they are typed, and they are bound to data eventually.

A variable name is used to refer to a piece of data, and an array name is used to refer to a set of data (a collection of data), both of which are typed in order to infer the length of the referred data.

Yes, arrays have types, too. We can think of int, float, char, and so on as primitive types, and arrays as slightly more complex types derived from primitive types

An array type consists of the type of the element and the length of the array. Sizeof calculates the length of a variable based on its type at compile time, not at runtime.

During compilation, the compiler creates a special table to hold variable names and their corresponding data types, addresses, scopes, and other information.

Sizeof is an operator, not a function, and when you use sizeof you can look up the length of the symbol from this table.

So, using sizeof on the name of the array will get you the actual length of the array.

Pa is just a pointer to an int, and the compiler doesn’t know whether it points to an integer or a bunch of integers.

Although in this case it refers to an array, the array is just a contiguous chunk of memory, with no start and end flags and no extra information to keep track of how long the array is.

So using sizeof on PA only yields the length of the pointer variable itself.

That is, the compiler does not associate PA with an array. Pa is just a pointer variable, and sizeof is always the number of bytes it takes up, no matter where it points to.

5.2 Two-dimensional Array

You should not think of a two-dimensional array in memory as a row, column, such as two-dimensional storage, in fact, regardless of the two-dimensional, three-dimensional array… All syntax candy for the compiler.

The storage is essentially the same as a one-dimensional array, for example:

You might expect an array in memory to look like a two-dimensional matrix:

But it actually looks like this:

It’s no different than a one-dimensional array, it’s a one-dimensional linear array.

When we access like array[1][1], how does the compiler calculate the address of the element we’re actually accessing?

For more generality, suppose the array definition looks like this:

int array[n][m]

Access: array [a] [b]

Array + (m * a + b)

This is the essence of a two-dimensional array in memory, which is exactly the same as a one-dimensional array, except that the syntax sugar is wrapped in a two-dimensional form.

NO.6

The magic void pointer

You must have seen these uses of void:

In these cases, void means that there is no return value or that the argument is null.

A void pointer, however, represents a generic pointer that can be used to hold references to any data type.

The following example is a void pointer:

The greatest use of void Pointers is for generic programming in C, because any pointer can be assigned to a void pointer, and a void pointer can be converted back to its original type without changing the address that the pointer actually points to.

Such as:

Both outputs will have the same value:

This conversion is rarely done, but when you’re writing large software in C or general purpose libraries, void Pointers are a cornerstone of C generics. For example, the STD library’s sort declaration looks something like this:

All references to specific element types are replaced with void.

Void can also be used to implement polymorphism in C, which is a fun thing to do.

But there are caveats:

– Void Pointers cannot be dereferenced

Such as:

Why is that?

Because the essence of dereference is that the compiler takes N consecutive bytes from the memory to which the pointer points based on the type to which the pointer points, and interprets those N bytes as the type of the pointer.

For example, an int * pointer, where N is 4, then interprets the number in the way int is encoded.

The compiler does not know whether a void pointer refers to an int, a double, or a structure, so it cannot dereference a void pointer.

NO.7

Fancy technique

Many students think that C is only procedural programming. In fact, we can simulate objects, inheritance, polymorphism and so on in C by using Pointers and structures.

You can also use void Pointers for generic programming, that is, templates in Java and C++.

If you are interested in C implementation of object-oriented, template, inheritance, you can be a little positive, like, leave a message ~ voice high, I will write another article.

And it’s actually kind of interesting, because once you know how to implement these things in C, you’ll understand objects in C++, objects in Java a lot better.

For example, why is there a this pointer, or what is self in Python?

There’s a lot more I want to write about Pointers, and this is just the beginning. I’m limited in space, but I’ll have a chance to fill in the following:

Two-dimensional arrays and two-dimensional Pointers

Array Pointers and pointer arrays

Pointer arithmetic

A function pointer

Dynamic memory allocation: malloc and free

Heap, stack

How function arguments are passed

Memory leaks

Arrays degenerate to Pointers

Const modifier pointer

.

It basically covers the core knowledge of C language.