There are dry goods, more stories, wechat search [programming refers to north] pay attention to this different programmer, waiting for you to hit ~

Note: This article will give you a good grasp of the essence of Pointers

The core knowledge of C language is pointer. Therefore, the topic of this article is “Pointer and Memory Model”.

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.

First, the 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. (Overconfidence

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, made up of a series of contiguous storage units, like this one,

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:

int a = 999;
char c = 'c';
Copy the code

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.

This way of putting the most important bytes in the lowest memory address is called big endian

On the other hand, the way in which the low byte is placed at the low 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.

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:

printf("%x", &a);
Copy the code

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:

int *pa = &a; 
Copy the code

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 I just use the variable name?

Sure, but variable names are limited.

What is the nature of a variable name?

It’s the symbol of the address of the variable, and the variable is to make our programming more convenient and friendly, but the computer doesn’t know what variable A is, it only knows the address and the instruction.

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:

a  | 0x7ffcad3b8f3c
c  | 0x7ffcad3b8f2c
h  | 0x7ffcad3b8f4c.Copy the code

Say of good!

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

int func(...). {... };int main(a) {
	inta; func(...) ; };Copy the 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:

int func(int address) {
  ....
};

int main() {
	int a;
	func(&a);
};
Copy the code

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?

Like int Pointers, float Pointers, does that affect the information that the pointer itself stores?

When does 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:

float f = 1.0;
short c = *(short*)&f; 
Copy the code

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. &fachieve fThe first address
  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?

short c = 1;
float f = *(float*)&c;
Copy the code

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:

* (float*)&c = 1.0;
Copy the code

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:

char buffer[4]; . printf("%f %x\n", *buffer, *buffer);Copy the code

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:

printf("%f %x\n", * (float*)buffer, *(float*)buffer);
Copy the code

It explicitly tells the compiler:

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

Structure and pointer

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

Such as:

struct fraction {
	int num; // The integer part
	int denom; // The decimal part
};

struct fraction fp;
fp.num = 10;
fp.denom = 2;
Copy the code

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:

((fraction*)(&fp.denom))->num = 5; 
((fraction*)(&fp.denom))->denom = 12; 
printf("%d\n", fp.denom); // How much output?
Copy the code

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.

4. Multi-level 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 guess many students are in the same situation 🤣

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.

int a;
int *pa = &a;
int **ppa = &pa;
int ***pppa = &ppa;
Copy the code

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 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 is a pointer variable, and int* *a is a pointer variable

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 only hold the address of int*** variables.

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:

int array[10] = {10.9.8.7};
printf("%d\n", *array);  / / output 10
printf("%d\n".array[0]);  / / output 10

printf("%d\n".array[1]);  / / output 9
printf("%d\n", * (array+1)); / / output 9

int *pa = array;
printf("%d\n", *pa);  / / output 10
printf("%d\n", pa[0]);  / / output 10

printf("%d\n", pa[1]);  / / output 9
printf("%d\n", *(pa+1)); / / output 9
Copy the code

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:

printf("%u".sizeof(array));
printf("%u".sizeof(pa));
Copy the code

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:

An int array [3] [3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; array[1][1] = 5;Copy the code

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

1, 2, 3, 4, 5, 6, 7, 8, 9Copy the code

But it actually looks like this:

1, 2, 3, 4, 5, 6, 7, 8, 9Copy the code

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.

Magic void pointer

You must have seen these uses of void:

void func(a);
int func1(void);
Copy the code

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:

void *ptr;
Copy the code

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:

int num;
int *pi = # 
printf("address of pi: %p\n", pi);
void* pv = pi;
pi = (int*) pv; 
printf("address of pi: %p\n", pi);
Copy the code

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:

void qsort(void *base,int nelem,int width,int (*fcmp)(const void *,const void *));
Copy the code

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:

int num;
void *pv = (void*)#
*pv = 4; / / error
Copy the code

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.

Seven, fancy show skills

A lot of students think that C is only procedural programming, but in fact, we can simulate objects, inheritance, polymorphism and so on in C by using Pointers.

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
  • .

omg

I’d like to write a series about memory, Pointers, references, function calls, stacks, object-oriented implementations, and so on.

I don’t know if you are interested, but if you are, please give me a thumbs up or check it out. If there are enough of them, I will keep writing.

For everyone prepared C language learning materials, very classic book, go to the public number “programming refers to the north” background reply “C language” can receive ~

The article is constantly updated. You can search “Programming refers to North” on wechat and read it in the first time. There are programming e-books and interview materials prepared by me