Three technical difficulties in embedded C language

​C language is essential knowledge in embedded learning, and even most operating systems are built around C language. Among them, there are three technical difficulties , which are almost recognized as " hard bones ".

Today I will take you to dismantle these three hard bones in detail, so that you can understand them clearly.

picture

picture

0x01 pointer

Pointers are recognized as the most difficult concept to understand, and are also the direct reason why many beginners choose to give up.

The reason why pointers are difficult to understand is that the pointer itself is a variable. It is a very special variable that specifically stores addresses. This address needs to apply for space to hold things, and because it is a variable that can be assigned values ​​in the middle, many people are troubled by this. I started to feel dizzy and couldn't get around the bend.

The reason why C language is liked by many experts is the charm of pointers, which can be flexibly switched in the middle and the execution efficiency is super high. This is also what makes novices confused.

Pointers are a knowledge point that cannot be bypassed in learning, and after learning C language, the next step is to switch to data structures and algorithms. Pointers are the focus of the switch. If you can’t figure out pointers, it will be difficult to proceed to the next step, which will make many people give up. The courage to learn.

Pointers are directly connected to the memory structure. It is common for pointers in C language to point randomly. The root cause of array out-of-bounds is memory problems. There is endless room for play at this point of the pointer. Many programming skills are gathered here.

Pointers also involve how to apply for the release of memory. If the release is not timely, memory leaks will occur. Pointers are efficient and easy to use, but if they are not fully understood, it is simply a nightmare for some people.

In terms of pointers, you can refer to the experience of the master:

Complex type description

To understand pointers, there will be some more or less complex types. So first introduce how to fully understand a complex type.

It is actually very simple to understand complex types. There will be many operators in a type. They also have priority like ordinary expressions. Their priority is the same as the operation priority.

So the author summarized its principles: start from the variable name, combine it according to the operator priority, and analyze step by step.

Let's start with simple types and analyze them slowly.

int p;

This is an ordinary integer variable.

int *p;

First, starting from p, it is combined with *, so it means that p is a pointer. Then combined with int, it shows that the type of the content pointed to by the pointer is int type, so p is a pointer that returns integer data.

int p[3];

First, start from p and combine it with [] to show that p is an array. Then combined with int, it shows that the elements in the array are integers, so p is an array composed of integer data.

int *p[3];

First, start from p and combine it with [] first. Because its priority is higher, p is an array. Then combine it with * to indicate that the elements in the array are pointer types. Then combined with int, it shows that the type of the content pointed to by the pointer is integer, so p is an array composed of pointers that return integer data.

int (*p)[3];

First, start from p and combine it with * to show that p is a pointer. Then combine it with [] (this step with "()" can be ignored, just to change the priority), indicating that the content pointed by the pointer is an array. Then combine it with int to indicate that the elements in the array are integers. So p is a pointer to 3 integers composed of integer data.

int **p;

First, start with p and combine it with * to show that p is a pointer. Then combine it with * to indicate that the element pointed to by the pointer is a pointer. It is then combined with int to indicate that the element pointed to by the pointer is integer data. Since second-level pointers and higher-level pointers are rarely used in complex types, we will not consider multi-level pointers for more complex types later, and only consider first-level pointers at most.

int p(int);

Starting from p, first combine it with () to show that p is a function. Then enter () to analyze, indicating that the function has a parameter of an integer variable, and then combined with the external int, indicating that the return value of the function is an integer data.

int (*p)(int);

Starting from p, first combine it with the pointer, indicating that p is a pointer. Then combined with (), it shows that the pointer points to a function. Then it is combined with the int in (), indicating that the function has an int type parameter, and then combined with the outermost int, indicating that the return type of the function is an integer, so p is a pointer with an integer parameter and a return type A pointer to an integer function.

int (*p(int))[3];

You can skip it first, don't look at this type, it is too complicated. Starting from p, first combine it with () to show that p is a function. Then enter () and combine it with int, indicating that the function has an integer variable parameter. Then combined with the * outside, it shows that the function returns a pointer. Then go to the outermost layer and combine it with [] first, indicating that the returned pointer points to an array. Then it is combined with int, indicating that the elements in the array are pointers, and finally combined with int, indicating that the content pointed to by the pointer is integer data. So p is a function that takes an integer as a parameter and returns a pointer variable pointing to an array of integer pointer variables.

That’s pretty much it. After understanding these types, other types are also a piece of cake for us. However, we generally do not use too complex types, which will greatly reduce the readability of the program, so please use them with caution. The above types are enough for us.

Details about pointers

A pointer is a special variable in which the value stored in it is interpreted as an address in memory.

To understand a pointer, you need to understand four aspects of the pointer: the type of the pointer, the type pointed by the pointer, the value of the pointer or the memory area pointed by the pointer, and the memory area occupied by the pointer itself. Let's explain each.

First declare a few pointers as an example:

(1)int *ptr;

(2)char *ptr;

(3)int **ptr;

(4)int (*ptr)[3];

(5)int *(*ptr)[4];

pointer type

From a grammatical point of view, friends only need to remove the pointer name in the pointer declaration statement, and the remaining part is the type of the pointer. This is the type of the pointer itself.

Let's look at the types of each pointer in the above example:

(1)int ptr;//指针的类型是int

(2)char ptr;//指针的类型是char

(3)int ptr;//指针的类型是int

(4)int (ptr)[3];//指针的类型是int()[3]

(5)int *(ptr)[4];//指针的类型是int(*)[4]

How about it? Isn't it a simple way to find out the type of a pointer?

the type pointed to by the pointer

When the memory area pointed to by the pointer is accessed through a pointer, the type pointed to by the pointer determines what the compiler will treat the contents of that memory area as .

From a syntactic point of view, friends only need to remove the pointer name in the pointer declaration statement and the pointer declarator * to the left of the name, and what is left is the type pointed by the pointer.

The types pointed to by each pointer in the above example:

(1)int ptr; //指针所指向的类型是int

(2)char *ptr; //指针所指向的的类型是char*

(3)int *ptr; //指针所指向的的类型是int*

(4)int (*ptr)[3]; //指针所指向的的类型是int(*)[3]

(5)int *(*ptr)[4]; //指针所指向的的类型是int*(*)[4]

In pointer arithmetic operations, the type pointed to by the pointer plays a large role.

The type of the pointer (that is, the type of the pointer itself) and the type pointed to by the pointer are two concepts. As friends become more and more familiar with C, they will find that dividing the concept of "type" that is mixed with pointers into two concepts: "type of pointer" and "type pointed to by the pointer" is a must for those who are proficient in pointers. One of the key points.

The author read a lot of books and found that some poorly written books mixed the two concepts of pointers together, so the books seemed inconsistent and the more they read, the more confused they became.

pointer value

That is, the memory area or address pointed by the pointer.

The value of the pointer is the value stored in the pointer itself. This value will be treated by the compiler as an address, not a general value.

In 32-bit programs, the value of all types of pointers is a 32-bit integer, because the memory addresses in 32-bit programs are all 32 bits long. The memory area pointed to by the pointer is a memory area starting from the memory address represented by the value of the pointer and having a length of sizeof (the type pointed to by the pointer).

From now on, when we say that the value of a pointer is XX, it is equivalent to saying that the pointer points to a memory area with the address headed by XX; when we say that a pointer points to a certain memory area, it is equivalent to saying that the value of the pointer is this The first address of the memory area.

The memory area pointed by the pointer and the type pointed by the pointer are two completely different concepts. In Example 1, the type pointed to by the pointer already exists, but because the pointer has not been initialized, the memory area it points to does not exist, or is meaningless.

In the future, every time you encounter a pointer, you should ask: What is the type of this pointer? What type does the pointer point to? Where does this pointer point to?

The memory area occupied by the pointer itself

How much memory does the pointer itself occupy? Just use the function sizeof (type of pointer) to test it and you will know. On 32-bit platforms, the pointer itself occupies 4 bytes in length. The concept of memory occupied by the pointer itself is useful in determining whether a pointer expression is an lvalue.

0x02 function

The basic unit of the process-oriented object module, and the corresponding various combinations, function pointers, and pointer functions.

A function is a business logic block, which is process-oriented and the smallest unit of the unit module. During the execution of the function, how the formal parameters and actual parameters exchange data, how to transfer the data, and how to design a reasonable function are not only To solve a function, it also depends on whether it can be reused to avoid reinventing the wheel.

Function pointers and pointer functions are interchangeable literal meanings, but actually have completely different meanings. Pointer functions are easy to understand, as they are functions that return pointers; function pointers are mainly used in callback functions. Many people feel that functions are not yet understood, and callback functions are even more confusing. In fact, we can generally understand that a pointer to a function is itself a pointer variable, but it points to the function during initialization, which is back to the pointer level. I didn’t understand that it’s extremely difficult to move forward when the pointer goes deeper again.

picture

The developers of the C language did some things to save effort for later developers. They wrote a lot of code and completed all common basic functions so that others can use them directly. But with so many codes, how do you find what you need? It is obviously unrealistic to take all the code.

However, these codes have long been classified into different files by early developers, and each piece of code has a unique name. So in fact, learning C language is not that difficult, especially if you can do it through hands-on exercises and projects. When using the code, just add () after the corresponding name. Such a piece of code is a function. A function can independently complete a certain function and can be used multiple times after being written once.

Many beginners may confuse the concepts of functions in C language and functions in mathematics. In fact, the truth is not that complicated. The functions in C language have rules to follow. As long as you understand the concept, you will find it quite interesting. The English name of the function is Function, which corresponds to the translated Chinese meaning of "function". Functions in C language are also closely related to functions.

Let's look at a small piece of C language code:

#include<stdio.h>
int main()
{
    puts("Hello World");
    return 0;
}

Focus on line 4, which prints "Hello World" to the monitor. We have already mentioned before that puts must be followed by (), and strings must also be placed in ().

In C language, some statements cannot be used with parentheses, and some statements must be used with parentheses. The one with parentheses is a function.

C language provides many functions, and we only need a simple code to use them. However, the underlying functions of these functions are relatively complex, usually a combination of software and hardware, and many details and boundaries must be considered. If these functions are left to programmers to complete, it will greatly increase the programmer's learning cost and reduce programming efficiency.

With functions, the programming efficiency of C language is like having an artifact. Developers only need to call it at any time. Process functions, operation functions, time and date functions, etc. can help us directly realize the functions of C language itself. .

C language functions can be reused .

An obvious feature of a function is that it must be used with parentheses (). If necessary, the parentheses can also contain the data to be processed. For example, puts (" Learn Embedded Together ") uses a piece of code with output function. The name of this piece of code is puts, and " Learn Embedded Together " is the data to be processed by this piece of code. Using functions has a professional name in programming, called function call.

If the function needs to process multiple data, use commas to separate them, for example:

pow(10, 2);

This function is used to find 10 raised to the power of 2.

Okay, after reading this, do you think that C language functions are actually quite interesting, and they are not that complicated and difficult. When you meet a newbie again in the future, you may be able to attract countless admiring looks on the spot as you speak a C language function.

0x03 structure, recursion

Many students are studying C language in college. They have not finished many courses and have not learned structures. Because from the arrangement of chapters, it seems that the study of structures is placed in the second half of the textbook, which makes many students feel that The structure is not important. If it is just to cope with the school exam, or just to get a diploma, there is indeed little meaning in learning.

If you want to work in the programming industry and don’t understand this concept, you basically cannot construct a data model. No business is completely completed using native data types. When many experts design data models, they usually first add the structure in the header file. Body data is sorted out. Then design the parameters and names of the functional functions, and then actually start writing the C source code.

If the data in the structure is placed in a different order from the perspective of saving space, the space occupied in the memory is also different. Values ​​are assigned between structures. If there are pointers in the structure, special attention must be paid to the assignment, and deep assignment is required.

Recursion is generally used to count or list some data from beginning to end. Many beginners feel awkward when using it. How can they still call themselves? And when using it, you must set the conditions for jumping out, otherwise it will continue endlessly and it will become an infinite loop.

For knowledge about structures, you can also refer to the experience of the boss:

I believe everyone is familiar with structures. Here, I would like to share my summary of my research and study on C language structures. If you find that there is something in this summary that you have not mastered before, then this article is of some value. Of course, the level is limited. If you find any shortcomings, please point them out. I put the code file test.c below. Here, I will analyze and apply C language structures around the following two issues:

  1. What is the function of structures in C language

  2. What is the importance of memory alignment of structure member variables (key points)

For the explanation of some concepts, I will not copy the definitions from the C language textbook. Let's sit down and talk slowly.

1. What is the function of structure?

Three months ago, a senior in the teaching and research department encountered this problem during an interview at Huawei's Nanjing Research Institute. Of course, this is just the most basic question in an interview. If asked, how would you answer? My understanding is this, structures in C language have at least the following three functions:

(1) Organizes the attributes of the object organically.

For example, in the RTC development of STM32, we need data to represent date and time. These data are usually year, month, day, hour, minute, and second. If we don't use a structure, we need to define 6 variables to represent it. In this case, the data structure of the program is loose, and our data structure is best "high cohesion, low coupling". Therefore, it is better to use a structure to represent it, whether in terms of readability, portability or maintainability of the program:

typedef struct //公历日期和时间结构体
{
vu16 year;
vu8 month;
vu8 date;
vu8 hour;
vu8 min;
vu8 sec;
}_calendar_obj;
_calendar_obj calendar; //定义结构体变量

(2) The method of modifying the structure member variables replaces the redefinition of the function (entry parameter).

If the structure organically organizes the properties of the object, it means that the structure is "intermediate", then the method of modifying the member variables of the structure instead of redefining the function (entry parameter) represents the "intermediate use" of the structure. Let's continue taking the above structure as an example and analyze it. Suppose now I have the following function to display date and time:

void DsipDateTime( _calendar_obj DateTimeVal)

Then we only need to call DsipDateTime() with a variable of the structure type _calendar_obj as an actual parameter. DsipDateTime() uses the variable of DateTimeVal to display the content. If we don't use a structure, we will probably need to write a function like this:

void DsipDateTime( vu16 year,vu8 month,vu8 date,vu8 hour,vu8 min,vu8 sec)

Obviously, such formal parameters are not impressive, and the data structure management is also very cumbersome. If the return value of a function is a data representing a date and time, it is more complicated. This is only one aspect.

On the other hand, if the user needs to include the day of the week (week) in the data representing the date and time, at this time, if the structure has not been used before, then another formal parameter vu8 week should be added to the DsipDateTime() function:

void DsipDateTime( vu16 year,vu8 month,vu8 date,vu8 week,vu8 hour,vu8 min,vu8 sec)

It can be seen that this method of passing parameters is very cumbersome. Therefore, one of the benefits of using a structure as the entry parameter of a function is that the function declaration void DsipDateTime(_calendar_obj DateTimeVal) does not need to be changed. You only need to add the member variables of the structure, and then make corresponding adjustments to calendar.week in the internal implementation of the function. Just process it. In this way, it plays a significant role in program modification and maintenance.

typedef struct //公历日期和时间结构体
{
vu16 year;
vu8 month;
vu8 date;
vu8 week;
vu8 hour;
vu8 min;
vu8 sec;
}_calendar_obj;
_calendar_obj calendar; //定义结构体变量

(3) The memory alignment principle of the structure can improve the CPU's access speed to the memory (exchanging space for time).

Moreover, the address of the structure member variable can be calculated based on the base address (offset). Let's first take a look at the following simple program. The analysis of this program will be explained in detail in Part 2: Memory Alignment of Structure Member Variables.

#include<stdio.h>

int main()
{
    struct    //声明结构体char_short_long
    {
        char  c;
        short s;
        long  l;
    }char_short_long;

    struct    //声明结构体long_short_char
    {
        long  l;
        short s;
        char  c;
    }long_short_char;

    struct    //声明结构体char_long_short
    {
        char  c;
        long  l;
        short s;
    }char_long_short;

printf(" \n");
printf(" Size of char   = %d bytes\n",sizeof(char));
printf(" Size of shrot  = %d bytes\n",sizeof(short));
printf(" Size of long   = %d bytes\n",sizeof(long));
printf(" \n");  //char_short_long
printf(" Size of char_short_long       = %d bytes\n",sizeof(char_short_long));
printf("     Addr of char_short_long.c = 0x%p (10进制:%d)\n",&char_short_long.c,&char_short_long.c);
printf("     Addr of char_short_long.s = 0x%p (10进制:%d)\n",&char_short_long.s,&char_short_long.s);
printf("     Addr of char_short_long.l = 0x%p (10进制:%d)\n",&char_short_long.l,&char_short_long.l);
printf(" \n");

printf(" \n");  //long_short_char
printf(" Size of long_short_char       = %d bytes\n",sizeof(long_short_char));
printf("     Addr of long_short_char.l = 0x%p (10进制:%d)\n",&long_short_char.l,&long_short_char.l);
printf("     Addr of long_short_char.s = 0x%p (10进制:%d)\n",&long_short_char.s,&long_short_char.s);
printf("     Addr of long_short_char.c = 0x%p (10进制:%d)\n",&long_short_char.c,&long_short_char.c);
printf(" \n");

printf(" \n");  //char_long_short
printf(" Size of char_long_short       = %d bytes\n",sizeof(char_long_short));
printf("     Addr of char_long_short.c = 0x%p (10进制:%d)\n",&char_long_short.c,&char_long_short.c);
printf("     Addr of char_long_short.l = 0x%p (10进制:%d)\n",&char_long_short.l,&char_long_short.l);
printf("     Addr of char_long_short.s = 0x%p (10进制:%d)\n",&char_long_short.s,&char_long_short.s);
printf(" \n");
return 0;
}

The running results of the program are as follows (note: the data in brackets are the decimal form of the address of the member variable):

picture

2. Memory alignment of structure member variables

First, let's analyze the running results of the above program. The first three lines indicate that in my program, the char type occupies 1 byte, the short type occupies 2 bytes, and the long type occupies 4 bytes. char_short_long, long_short_char and char_long_short are three structures with the same members but the order of member variables is different. And judging from the running results of the program,

Size of char_short_long = 8 bytes
Size of long_short_char = 8 bytes
Size of char_long_short = 12 bytes //比前两种情况大4 byte !

Also, note that 1 byte (char) + 2 byte (short) + 4 byte (long) = 7 byte, not 8 byte.

Therefore, the order in which the structure member variables are placed affects the size of the memory space occupied by the structure. The memory size occupied by a structure variable is not necessarily equal to the sum of the space occupied by its member variables. If there are a large number of structure variables in a user program or operating system (such as uC/OS-II), this memory usage must be optimized. In other words, the order of member variables within the structure must be particular.

How are structure member variables stored?

Here, I will not be too pretentious and directly give the following conclusion, without the #pragma pack macro:

  • Principle 1 For data members of a structure (struct or union), the first data member is placed at offset 0. The starting position of each subsequent data member storage should start from an integer multiple of the member size (for example, int is at 32 The bit machine is 4 bytes, so storage must start from the address that is an integer multiple of 4).

  • Principle 2: The total size of the structure, that is, the result of sizeof, must be an integer multiple of its largest internal member, and any shortage must be made up.

  • Principle 3: When a structure is used as a member, the structure members should be stored starting from the address that is an integral multiple of the largest internal element size. (When struct a contains struct b, and b contains elements such as char, int, double, etc., then b should be stored from an address that is an integer multiple of 8, because sizeof(double) = 8 bytes)

Here, we analyze it in conjunction with the above program (not discussing principle 3 for now).

Let's first look at the two structures char_short_long and long_short_char. From the addresses of their member variables, we can see that these two structures comply with principles 1 and 2. Note that in the address of the member variable of char_short_long, the address of char_short_long.s is 1244994. In other words, 1244993 is "empty" and is just a "placeholder"!

Member variables Member variable hexadecimal address Member variable decimal address
char_long_short.c 0x0012FF2C 1244972
char_long_short.l 0x0012FF30 1244976
char_long_short.s 0x0012FF34 1244980

It can be seen that the memory distribution diagram is as follows, with a total of 12 bytes:

picture

First of all, 1244972 is divisible by 1, so there is no problem in placing char_long_short.c at 1244972 (in fact, as far as the char type member variable itself is concerned, there is no problem in placing it at any address unit). According to principle 1, after 1244973 None of ~1244975 is divisible by 4 (because sizeof(long)=4bytes). 1244976 can be divisible by 4, so char_long_short.l should be placed at 1244976. In the same way, the last .s(sizeof(short)= 2 bytes) should be placed at 1244980.

Is this the end? No, there is still principle 2. According to the requirements of principle 2, the space occupied by the char_long_short structure should be an integer multiple of the size of its member variable that occupies the largest memory space. If we end here, then the memory space occupied by char_long_short is 1244972~1244981, a total of 10 bytes, which does not comply with principle 2. Therefore, 2 bytes (1244982~1244983) must be completed at the end.

At this point, the memory layout of a structure is completed.

Below we follow the above principles to verify whether this analysis is correct. According to the above analysis, address units 1244973, 1244974, 1244975 and 1244982, 1244983 are all empty (at least char_long_short is not used, it is just a "placeholder").

If our analysis is correct, then defining such a structure should occupy 12 bytes of memory:

struct //声明结构体char_long_short_new
{
char c;
char add1; //补齐空间
char add2; //补齐空间
char add3; //补齐空间
long l;
short s;
char add4; //补齐空间
char add5; //补齐空间
}char_long_short_new;

It can be seen that our analysis is correct. As for principle 3, you can program and verify it yourself, so we won’t discuss it here.

Therefore, whether you are in VC6.0, Keil C51, or Keil MDK, when you need to define a structure, as long as you pay a little attention to the memory alignment of structure member variables, you can save MCU to a great extent. of RAM. This applies not only to actual programming, but is also common in written examinations and interviews at many large companies, such as IBM, Microsoft, Baidu, and Huawei.

These three hard bones are the stumbling blocks for learning C language. If you work hard to remove the main arteries of C language, it will be opened up, and then it will be relatively simple to learn other content.

The more painful the programming learning process is, the more things will be learned. Overcoming the past will improve your own skills, and the time spent giving up the previous efforts will be cleared. The more difficult a language is to learn, the more enjoyable it will be after getting started, and it is also easy to become addicted. Are you addicted? Or gave up?

Guess you like

Origin blog.csdn.net/weixin_41114301/article/details/132916821