In-depth analysis of C language how to understand pointers and structure pointers, pointer functions, function pointers

1. Pointer variables

  • First of all, you must understand that a pointer is a variable, you can use the following code to verify:
#include "stdio.h"

int main(int argc, char **argv) {
    
    
    unsigned int a = 10;
    unsigned int *p = NULL;
    p = &a;
    printf("&a = %d\n",a);
    printf("&a = %d\n",&a);
    *p = 20;
    printf("a = %d\n",a);
    return 0;
}
  • The result of the operation is as follows:
a = 10
&a = 6422216
a = 20
  • It can be seen that the value of a has been changed, so it can be clearly understood that the pointer is essentially a special variable for placing the address of the variable, and its essence is still a variable. Since the pointer is a variable, there must be a variable type.
  • In C language, all variables have variable types, such as integer, floating type, character type, pointer type, structure, union, enumeration, etc., all of which are variable types. The emergence of variable types is the inevitable result of memory management. We all know that all variables are stored in the computer's memory. Since they are placed in the computer's memory, they will inevitably occupy a certain amount of space. How much space will a variable occupy? What about space? Or how much memory space should be allocated to place the variable?
  • In order to specify this problem, the type was born. For a 32-bit compiler, the int type occupies 4 bytes, that is, 32 bits, and the long type occupies 8 bytes, that is, 64 bits. In a computer, the programs to be run are stored in the memory, and all variables in the program are actually operations on the memory. The memory structure of the computer is relatively simple. Here we will not discuss the physical structure of the memory in detail, but only the memory model. The memory of the computer can be imagined as a house, in which people live, each room corresponds to the memory address of the computer, and the data in the memory is equivalent to the people in the house.

insert image description here

  • Since the pointer is also a variable, that pointer should also be stored in memory. For a 32-bit compiler, its addressing space is 2 32 = 4GB . In order to be able to operate all memory (in fact, it is impossible for ordinary users to operate all memory ), the pointer variable storage also needs to use 32 digits, that is, 4 bytes, so that there is the address of the pointer &p, the relationship between the pointer and the variable can be represented by the following figure:

insert image description here

  • It can be seen that &p is the address of the pointer, which is used to store the pointer p, and the pointer p is used to store the address of the variable a, which is &a, and there is a *p in C language that is "dequote", which means to tell the compiler to take out The content stored at this address. You can think for yourself: what do &(*p) and *(&p) mean, and how to understand them?
  • Regarding the question of the pointer type, for the 32-bit compiler, since any pointer only occupies 4 bytes, why do we need to introduce the pointer type? Is it just to constrain variables of the same type? In fact, I have to mention pointer operations, first think about the following two operations: p+1 and ((unsignedint)p)+1, how to understand?
  • The meanings of these two operations are different. First, let’s talk about the first p+1 operation, as shown in the following figure:

insert image description here

  • For different types of pointers, the address pointed to by p+1 is different. This increment depends on the memory size occupied by the pointer type, and for ((unsigned int)p)+1, it means that the address pointed to by p The value of the address is directly converted to a number, and then +1, so that no matter what type of pointer p is, the result is the address after the address pointed to by the pointer.
  • From the above analysis, it can be seen that the existence of pointers allows programmers to manipulate memory quite easily, which also makes some people think that pointers are quite dangerous. This view is reflected in C# and Java languages, but in fact, using pointers can Greatly improve efficiency.
  • Going a little deeper to operate on the memory through pointers, now you need to fill in a data 125 in the memory 6422216, you can perform the following operations:
unsigned int *p = (unsigned int*)(6422216);
*p = 125;
  • Of course, the above code uses a pointer. In fact, in C language, the dereferencing operation can be directly used to assign values ​​to memory more conveniently.

2. Interpretation

  • The so-called dequote operation is actually an operation on an address. For example, if you want to assign a value to the variable a now, the general operation is a = 125. Now use the dequote operation to complete it. The operation is as follows:
*(&a) = 125;
  • You can see that the dequote operator is *. This operator has two different meanings for pointers. When declaring, it declares a pointer, and when using p pointer, it is a dequote operation. The right side of the dequote operation is a The address, so the dequote operation means the data in the address memory, so to fill in a data 125 in the memory 6422216, the following operations can be used:
*(unsigned int*)(6422216) = 125;
  • The above operation converts the value of 6422216 into an address. This is to tell the compiler that the value is an address. It is worth noting that all memory addresses above cannot be specified casually. They must be memory allocated by the computer, otherwise the computer will think that the pointer is out of bounds And being killed by the operating system means that the program terminates early.

Three, the structure pointer

  • The structure pointer is the same as the ordinary variable pointer. The structure pointer only occupies 4 bytes (32-bit compiler), but the structure pointer can easily access any member of the structure type. This is the member operator of the pointer - >.
  • As shown below, p is a structure pointer, p points to the first address of a structure, and p->a can be used to access the member a in the structure, of course p->a and *§ are the same:

insert image description here

4. Mandatory type conversion

  • From the above test code, we can see that the compiler will report a lot of warnings, which means that the data types do not match. Although it does not affect the correct operation of the program, many warnings will always make people feel uncomfortable. So in order to tell the compiler that there is no problem with the code, a cast can be used to convert a piece of memory to the required data type.
  • There is an array a as follows, which is cast to a structure type stu:
#include <stdio.h>

typedef struct STUDENT {
    
    
    int name;
    int gender;
}stu;

int a[100] = {
    
    10,20,30,40,50};

int main(int argc, char **argv) {
    
    
    stu *student;
    student = (stu*)a;
    printf("student->name = %d\n", student->name);
    printf("student->gender = %d\n", student->gender);
    return 0;
}
  • The result of the operation is as follows:
student->name = 10
student->gender = 20
  • It can be seen that a[100] is casted to the stu structure type. Of course, it is also possible not to use forced type conversion, but the compiler will report an alarm. As shown below, the first 12 bytes of the array a[100] are forcibly converted to a struct stu type, which explains the array. The same is true for other data types, which are essentially a piece of memory space:

insert image description here

Five, void pointer

  • The void type is easy to think of as empty, but for pointers, it does not mean void, but uncertain. In many cases, the pointer may not know what type it is when declaring it, or there are multiple data types pointed to by the pointer, or you just want to operate a memory space through a pointer, at this time, you can declare the pointer as void type.
  • Then the problem comes, because of the void type, when dequoting a certain data type, the compiler will dereference the corresponding data according to the space occupied by the type, such as int p, then p will be dereferenced by the compiler as p The space size of the address of the pointer is 4 bytes. But for the null pointer type, how does the compiler know the size of the memory to be dereferenced?
  • First look at the following piece of code:
#include <stdio.h>

int main(int argc, char **argv) {
    
    
    int a = 10;
    void *p;
    p = &a;
    printf("p = %d\n",*p);
    return 0;
}
  • After compiling the above code, it can be found that the compiler reports an error and cannot compile normally:
error:invalid use of void expression
  • This shows that the compiler cannot determine the size of *p when dequoting, so it must tell the compiler the type of p or the size of *p, so how to tell? In fact, it is very simple, just use mandatory type conversion, as follows:
*(int*)p
  • So the above code can be optimized to:
#include <stdio.h>

int main(int argc, char **argv) {
    
    
    int a = 10;
    void *p;
    p = &a;
    printf("p = %d\n", *(int*)p);
    return 0;
}
  • The result of the operation is as follows:
p = 10
  • It can be seen that the result is indeed correct and consistent with the expected idea. Since the void pointer has no space size attribute, the void pointer has no ++ operation.
  • Summary: A void pointer is just a pointer without a specified type, that is, the pointer only has the address data attribute, and does not have the space size attribute when dequoting.

Six, function pointer

① Instructions for the use of function pointers

  • Function pointers are used a lot in the Linux kernel, and they are also used when designing the operating system. Since function pointers are also pointers, function pointers also occupy 4 bytes (32-bit compiler).
  • To illustrate with a simple example:
#include <stdio.h>

int  add(int a,int b) {
    
    
    return a+b;
}

int main(int argc, char **argv) {
    
    
    int (*p)(int,int);
    p = add;
    printf("add(10,20) = %d\n",(*p)(10,20));
    return 0;
}
  • The result of the operation is as follows:
add (10, 20) = 30
  • As you can see, the declaration of the function pointer is:
返回类型(*函数名)(参数列表)
  • The dereferencing operation of a function pointer is a bit different from that of ordinary pointers. For ordinary pointers, dereferencing only needs to retrieve the data according to the type, but the function pointer is to call a function, and its dereferencing cannot be the data Take out, in fact, the dereferencing of the function pointer is essentially the process of executing the function, but the call instruction used to execute the function is not the previous function, but the value of the function pointer, that is, the address of the function. In fact, the process of executing a function essentially uses the call instruction to call the address of the function, so the function pointer essentially saves the first address of the function execution process.
  • The function pointer is called as follows:
函数指针调用(*(实参列表)
  • To confirm that a function pointer is essentially the address of a function passed to the call instruction, two pieces of code are shown below:
#include <stdio.h>

void add (void) {
    
    
	printf("hello add\n");
}

int main (int arg, char **argv) {
    
    
	void (*p (void);
	p = add;
	(*р) О;
	return 0;
}
#include <stdio.h>

void add (void) {
    
    
	printf("hello add\n");
}

int main (int arg, char **argv) {
    
    
	add();
	return 0;
}
  • The assembly instructions after the two pieces of code are compiled are as follows:
0×4015d5 	push    ebp
0×4015d6	mov     ebp, esp
0×4015d8	and     esp, 0xfffffff0
0×4015db	sub     esp, 0x10
0×4015de	call    0x401690 <__main>
0×4015e3	mov     exa, DWORD PTR [esp+0xc]
0×4015eb	call    exa
0×4015ef	mov     exa 0x0
0×4015f1 	leave
0×4015f6    left
0×4015f7	ret
0×4015d5 	push    ebp
0×4015d6	mov     ebp, esp
0×4015d8	and     esp, 0xfffffff0
0×4015db	call    0x401680 <__main>
0×4015e0	call    0x4016c0 <add>
0×4015e5	mov     exa 0x0
0×4015ea	leave
0×4015eb    left
  • It can be seen that when using a function pointer to call a function, its assembly instructions are as follows:
0x4015e3    mov    DWORD PTR [esp+0xc],0x4015c0
0x4015eb    mov    eax,DWORD PTR [esp+0xc]
0x4015ef    call   eax
  • The first line of the mov instruction assigns the immediate value 0x4015c0 to the address memory of the register esp+0xc, then assigns the value of the address of the register esp+0xc to the register eax (accumulator), and then calls the call instruction. At this time, the pc pointer will point to add function, and 0x4015c0 is just the first address of function add, thus completing the function call. Careful, have you found an interesting phenomenon, the value of the function pointer in the above process is placed in the stack frame like the parameters, so it seems to be a process of parameter passing, so you can see that the function pointer is finally passed as parameters The form is passed to the called function, and the passed value happens to be the first address of the function.
  • Function pointers can not manipulate memory like ordinary pointers, so function pointers can be regarded as function reference declarations.

② Application of function pointer

  • It is most used in the Linux-driven object-oriented programming idea, and uses function pointers to realize encapsulation. As follows:
#include <stdio.h>

typedef struct TFT_DISPLAY {
    
    
    int   pix_width;
    int   pix_height;
    int   color_width;
    void (*init)(void);
    void (*fill_screen)(int color);
    void (*tft_test)(void);
}tft_display; 

static void init(void) {
    
    
    printf("the display is initialed\n");
}

static void fill_screen(int color) {
    
    
    printf("the display screen set 0x%x\n",color);

}

tft_display mydisplay = {
    
    
    .pix_width = 320,
    .pix_height = 240,
    .color_width = 24,
    .init = init,
    .fill_screen = fill_screen,
};

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

    mydisplay.init();
    mydisplay.fill_screen(0xfff);
    return 0;
}
  • The above sample code encapsulates a tft_display into an object. The last member of the structure is not initialized. It is used a lot in Linux. The most common one is the file_operations structure. Generally speaking, only common functions need to be initialized in this structure. All initialization is not required, and the structure initialization method used is also the most commonly used method in Linux. The advantage of this method is that it does not need to be one-to-one according to the order of the structure.

③ callback function

  • Sometimes there is such a situation that when A hands over a function to B for completion, A and B work synchronously. At this time, the function function has not been completed. At this time, A can define an API to hand over to B, and A As long as you care about the API, you don’t need to care about the specific implementation. The specific implementation can be completed by B. In this case, the callback function (Callback Function) will be used. Now suppose A needs an FFT algorithm. At this time, A will The FFT algorithm is handed over to B to complete, now let’s realize this process:
#include <stdio.h>

int InputData[100] = {
    
    0};
int OutputData[100] = {
    
    0};

void FFT_Function(int *inputData, int *outputData, int num) {
    
    
    while(num--) {
    
    

    }
}

void TaskA_CallBack(void (*fft)(int*,int*,int)) {
    
    
    (*fft)(InputData, OutputData,100);
}

int main(int argc, char **argv) {
    
    
    TaskA_CallBack(FFT_Function);
    return 0;
}
  • It can be seen that TaskA_CallBack is a callback function whose formal parameter is a function pointer, while FFT_Function is a called function, and the type of the function pointer declared in the callback function must be exactly the same as that of the called function.

Guess you like

Origin blog.csdn.net/Forever_wj/article/details/128847659