C language basics: the use of pointers

This article combines work experience to study the usage of pointers in C language.

1 The concept of pointers

Pointers are the essence of the C language and are used to store the addresses of variables. The value of the variable stored in this address can be accessed indirectly through the pointer. For pointers, you first need to understand the meanings of the & and * operators, for example as follows.

#include <stdio.h>

int main()
{
    
    
    int a = 1;
    int* p = &a;
    int b = *p;
    printf("变量a的地址是%p\r\n", p);
    printf("变量a的数值是%d\r\n", a);
    printf("变量b的数值是%d\r\n", *p);
}

First, define a variable a of type int and assign a value of 1 at the same time; then define a pointer p and assign it to the address of variable a (take the address through the & operator); then print out the address p of variable a and the value of variable a , and then print the value of the variable b, and obtain the variable in the p address through the * operator.

The above is a very basic example that should be mastered by a freshman in college. Based on the work experience, the blogger summarizes the scenarios where pointers are used in the development of automotive software C language.

2 Usage and usage scenarios

2.1 Pointer parameters of functions

2.1.1 Basic concepts

I have learned in college that the parameters of C language functions are formal parameters. Inside the function, no matter how the formal parameters are changed, the actual parameters outside the function cannot be changed. A typical example is to exchange the values ​​of a and b through a function, as follows.

#include <stdio.h>

void swap(int a, int b)
{
    
    
    int temp;
    temp = a;
    a = b;
    b = temp;
}

void swap_pointer(int* a_p, int* b_p)
{
    
    
    int temp;
    temp = *a_p;
    *a_p = *b_p;
    *b_p = temp;
}

int main()
{
    
    
    int a = 1;
    int b = 2;
    printf("a = %d, b = %d \r\n", a, b);
    swap(a, b);
    printf("After swap(a, b) : a = %d, b = %d \r\n", a, b);
    swap_pointer(&a, &b);
    printf("After swap_pointer(a, b) : a = %d, b = %d \r\n", a, b);

}

In the code, two functions swap and swap_pointer are defined. The former is a variable of int type, and a and b are exchanged inside the function; the latter is a pointer parameter, and the value is exchanged by address dereferencing inside the function. After running the code, as shown below:

insert image description here
This is because the function parameters passed by the former are formal parameters, which are just a copy of the parameters passed in from the outside, and the exchange of values ​​does not affect the outside; while the addresses passed in by the latter are the same as the addresses of a and b outside, so direct operation The memory space corresponding to the address can affect the outside of the function.

2.1.2 Use Scenario 1 - function returns multiple values

Using pointers as function parameters is more common in scenarios where functions need to output multiple return values.

When the function has only one output, return can be used to return the value. For example, the following code, by passing the radius of the circle as a parameter to the function, the function returns the circumference of the circle after calculation.

#include <stdio.h>

float calculate_perimeter(float radius)
{
    
    
    return 2 * 3.14 * radius;
}

int main()
{
    
    
    float radius = 2.0;
    float perimeter = calculate_perimeter(radius);
    printf("radius = %f, perimeter = %f \r\n", radius, perimeter);

}

By calling the calculate_perimeter() function, the perimeter corresponding to the radius is obtained from its return value. But if the requirements are more complicated, and you want to calculate the circumference and area of ​​the circle through the radius, if you still use the return value, you must design two functions, as follows.

#include <stdio.h>

float calculate_perimeter(float radius)
{
    
    
    return 2 * 3.14 * radius;//2*PI*R
}

float calculate_area(float radius)
{
    
    
    return 3.14 * radius * radius;//PI*R^2
}

int main()
{
    
    
    float radius = 2.0;
    float perimeter = calculate_perimeter(radius);
    float area = calculate_area(radius);
    printf("radius = %f, perimeter = %f, area = %f \r\n", radius, perimeter, area);
}

By passing parameters by pointer, you can design a function that returns two values, as follows.

#include <stdio.h>

void calculate_perimeter_area(float radius,float* perimeter_p,float* area_p)
{
    
    
    *perimeter_p = 2 * 3.14 * radius;//2*PI*R
    *area_p      = 3.14 * radius * radius;//PI*R^2
}

int main()
{
    
    
    float radius = 2.0;
    float perimeter, area;
    calculate_perimeter_area(radius, &perimeter, &area);
    printf("radius = %f, perimeter = %f, area = %f \r\n", radius, perimeter, area);
}

In addition, even if only one parameter is returned, it is often not returned by return. This is because the return value is used as a sign of whether the function ran successfully.

2.1.3 Use Scenario 2 - Reduce Function Parameters

Many enterprise specifications require that the function parameters of the C language be as few as possible, for example, the parameters of a function are less than 5. Such requirements are usually for the readability of the code and to save the use of stack space.

If a function has a lot of inputs, you can consider packing them into a structure, and then use the pointer of the structure variable as a function parameter. For example, the above functions for calculating the radius and circumference of a circle can be modified.

#include <stdio.h>

typedef struct Circle_Tag
{
    
    
    float Radius;
    float Perimeter;
    float Area;
} Circle_Type;

void calculate_perimeter_area(Circle_Type* circle_p)
{
    
    
    circle_p->Perimeter = 2 * 3.14 * circle_p->Radius;//2*PI*R
    circle_p->Area      = 3.14 * circle_p->Radius * circle_p->Radius;//PI*R^2
}

int main()
{
    
    
    Circle_Type circle;
    circle.Radius = 2.0F;
    calculate_perimeter_area(&circle);
    printf("radius = %f, perimeter = %f, area = %f \r\n", circle.Radius, circle.Perimeter, circle.Area);
}

In the above code, the radius, circumference, and area of ​​the circle are defined in the same structure type. The address of the structure variable is passed to the function as a parameter, so that only one address variable needs to be passed, and all the information of input and output can be obtained inside the function. At the same time, since only one address is passed, this function only uses 4 bytes of stack space. And passing three variables of type float requires 12 bytes.

2.2 void* pointer

2.2.1 Basic concepts

A void* pointer is a pointer that has no concrete type. Both the pointer of int type and the pointer of void type store an address, but since the memory space pointed to by the int type pointer is int type, the variable in the 4-byte space at the address can be obtained by dereferencing value. And the void* pointer does not know how many bytes this address occupies, so the value of the variable cannot be retrieved. Take a look at the following code:

#include <stdio.h>

int main()
{
    
    
	int a = 1;
	void* p = (void*)&a;
	int b = *p;
	printf("b = %d \r\n", b);
}

In the code, define void* to define pointer p, and assign the address of variable a to p. Then try to assign the value of the variable in the memory space pointed to by p to b by dereferencing. When running the code, an error will be reported as follows:
insert image description here
Because the pointer variable p only contains the address, without knowing the specific type, the value cannot be obtained from the address. The correct way is to cast the void* pointer first, and then dereference it.

#include <stdio.h>

int main()
{
    
    
	int a = 1;
	void* p = (void*)&a;
	int b = *(int*)p;
	printf("b = %d \r\n", b);
}

2.2.2 Usage Scenarios

When the void* type pointer is used, it also needs to be cast, which seems very troublesome, but there are also scenarios where it is used. When designing a function, it is necessary to clarify the data type of the parameter, which makes it difficult to unify and platformize the function in many cases. For example, you still need to design functions to calculate perimeter and area, but the input geometries are both circles and rectangles. Since the two geometric figures correspond to the two structure types, two functions must be designed to calculate the perimeter and area respectively. The code is as follows.

#include <stdio.h>

typedef struct Circle_Tag
{
    
    
    float Radius;
    float Perimeter;
    float Area;
} Circle_Type;

typedef struct Rectangle_Tag
{
    
    
    float Length;
    float Width;
    float Perimeter;
    float Area;
} Rectangle_Type;

void calculate_circle(Circle_Type* circle_p)
{
    
    
    circle_p->Perimeter = 2 * 3.14 * circle_p->Radius;//2*PI*R
    circle_p->Area = 3.14 * circle_p->Radius * circle_p->Radius;//PI*R^2
}

void calculate_rectangle(Rectangle_Type* rectangle_p)
{
    
    
    rectangle_p->Perimeter = 2 * (rectangle_p->Length + rectangle_p->Width);//2*(L+W)
    rectangle_p->Area = rectangle_p->Length * rectangle_p->Width;//PI*R^2
}

int main()
{
    
    
    Circle_Type circle;
    circle.Radius = 2.0F;
    calculate_circle(&circle);
    printf("Circle: Radius = %f, Perimeter = %f, Area = %f \r\n", circle.Radius, circle.Perimeter, circle.Area);

    Rectangle_Type rectangle;
    rectangle.Length = 2.0F;
    rectangle.Width = 3.0F;
    calculate_rectangle(&rectangle);
    printf("Rectangle: Length = %f, Width = %f, perimeter = %f, area = %f \r\n", rectangle.Length, rectangle.Width, rectangle.Perimeter, rectangle.Area);
}

In the code, due to the different types of structures, two functions must be designed to input different parameters to process the calculation of two kinds of geometric figures respectively.

Using the void* type pointer, it can be designed as a function. code show as below:

#include <stdio.h>

typedef struct Circle_Tag
{
    
    
    float Radius;
    float Perimeter;
    float Area;
} Circle_Type;

typedef struct Rectangle_Tag
{
    
    
    float Length;
    float Width;
    float Perimeter;
    float Area;
} Rectangle_Type;

typedef enum Geometry_Tag
{
    
    
    Geometry_Circle,
    Geometry_Rectangle
} Geometry_Type;

void calculate_geometry(void* geometry, Geometry_Type geometry_type)
{
    
    
    if (geometry_type == Geometry_Circle)
    {
    
    
        ((Circle_Type*)geometry)->Perimeter = 2 * 3.14 * ((Circle_Type*)geometry)->Radius;//2*PI*R;
        ((Circle_Type*)geometry)->Area = 3.14 * ((Circle_Type*)geometry)->Radius * ((Circle_Type*)geometry)->Radius;//PI*R^2
    }
    else if (geometry_type == Geometry_Rectangle)
    {
    
    
        ((Rectangle_Type*)geometry)->Perimeter = 2 * (((Rectangle_Type*)geometry)->Length + ((Rectangle_Type*)geometry)->Width);//2*(L+W)
        ((Rectangle_Type*)geometry)->Area = ((Rectangle_Type*)geometry)->Length * ((Rectangle_Type*)geometry)->Width;//PI*R^2
    }
    else
    {
    
    
        //do nothing
    }
}

int main()
{
    
    
    Circle_Type circle;
    circle.Radius = 2.0F;
    calculate_geometry((void*)(&circle), Geometry_Circle);
    printf("Circle: Radius = %f, Perimeter = %f, Area = %f \r\n", circle.Radius, circle.Perimeter, circle.Area);

    Rectangle_Type rectangle;
    rectangle.Length = 2.0F;
    rectangle.Width = 3.0F;
    calculate_geometry((void*)(&rectangle), Geometry_Rectangle);
    printf("Rectangle: Length = %f, Width = %f, perimeter = %f, area = %f \r\n", rectangle.Length, rectangle.Width, rectangle.Perimeter, rectangle.Area);
}

Through the second parameter of the calculate_geometry function, it can be judged whether the void* pointer of the first parameter is converted from a circle type or a rectangle type, so that the void* pointer is forced to be converted back to the original type inside the function, and then used to perform corresponding calculation.

The calculate_geometry function uses a void* type parameter, which can be called a weak type parameter. Parameters with clearly defined types, such as float and int, are called strongly typed parameters. For the above scenario where the functional interface needs to be generic, weak type parameters can be used.

2.3 Null pointers

When defining a pointer, if it is not assigned immediately, the pointer will point to a random address. A better way is to assign a value of NULL when defining the pointer, which is NULL in C language, as follows.

#include <stdio.h>

int main()
{
    
    
    int* p = NULL;
}

This ensures that the address of the pointer is 0, but the pointer still cannot be dereferenced, because the programmer should assign a meaningful address to the pointer in order to retrieve the variable from the address of the memory. If you dereference a null pointer, an error will still be reported, such as the following code.

#include <stdio.h>

int main()
{
    
    
    int* p = NULL;
    int a = *p;

    printf("a = %d \r\n", a);
}

After running in visual studio, an error will be reported.

insert image description here
However, if the same code is put in other compilers, an error may not be reported. For example, the elf file can be successfully generated by compiling code for embedded hardware through Hightec or Tasking compiler. However, when the software is flashed to the embedded controller, the hardware operation will be stuck. It takes a lot of energy to debug on the hardware to locate this problem.

When writing code, you should pay attention to verifying whether the pointer is a null pointer. For example, the above function of calculating the perimeter area of ​​a circle can be used to perform a null pointer check.

#include <stdio.h>

typedef struct Circle_Tag
{
    
    
    float Radius;
    float Perimeter;
    float Area;
} Circle_Type;

int calculate_perimeter_area(Circle_Type* circle_p)
{
    
    
    int retVal = 0;
    if (NULL == circle_p)//校验是否为空指针
    {
    
    
        retVal = 0;
    }
    else
    {
    
    
        circle_p->Perimeter = 2 * 3.14 * circle_p->Radius;//2*PI*R
        circle_p->Area = 3.14 * circle_p->Radius * circle_p->Radius;//PI*R^2
        retVal = 1;
    }    
    return retVal;//返回校验的结果
}

int main()
{
    
    
    Circle_Type circle;
    circle.Radius = 2.0F;
    if (calculate_perimeter_area(&circle))
    {
    
    
        printf("radius = %f, perimeter = %f, area = %f \r\n", circle.Radius, circle.Perimeter, circle.Area);
    }
    else
    {
    
    
        printf("Function failed!");
    }    
}

If the function is designed in this way, the caller of the function can be prompted by the return value whether the function call fails, so as to find out that the parameter passed a null pointer.

2.4 const pointer

2.4.1 Basic concepts

When the const keyword modifies a variable, it means that the value of this variable cannot be changed and needs to be assigned immediately when it is defined, and it cannot be changed later. When the const keyword modifies the pointer, the characteristics of the pointer are different according to the position of the const.

1) The following code is a constant pointer. When defining a pointer, first write const, then write int*.

const int* p = &a;

Since const is before int*, the meaning of const here is that the value of the memory pointed to by the pointer is constant, and this value cannot be modified. For example, if the following code tries to modify the value pointed to by the constant pointer, an error will be reported.

#include <stdio.h>

int main()
{
    
    
    int a = 10;
    const int* p = &a;
    *p = 20;
}

After running the code, an error will be reported as follows:
insert image description here
Here the compiler prompts that the constant cannot be assigned a value. However, the address pointed by the pointer can be modified, such as the following code.

#include <stdio.h>

int main()
{
    
    
    int a = 10;
    const int* p = &a;
    int b = 20;
    p = &b;
}

2) The following code is a pointer constant. When defining a pointer, first write int*, and then write const.

int* const p = &a;

Since int* is before const, the meaning of const here is that the address pointed to by the pointer is constant, and the address it points to cannot be changed. For example, if the following code tries to modify the address pointed to by the pointer constant, an error will be reported.

#include <stdio.h>

int main()
{
    
    
    int a = 10;
    int* const p = &a;
    int b = 20;
    p = &b;
}

After running the code, an error will also be reported.
insert image description here
This means that the address pointed to by the pointer cannot be assigned another address. However, the value of the memory address pointed to by the pointer can be modified, such as the following code.

#include <stdio.h>

int main()
{
    
    
    int a = 10;
    int* const p = &a;
    int b = 20;
    *p = b;
}

3) Combining the above two consts becomes a constant pointer to a constant, which means that neither the address nor the value can be changed.

const int* const p = &a;

No more specific examples.

2.4.2 Usage Scenarios

The main usage scenario of the const modified pointer is when the parameter of the function is a pointer. When a function parameter is passed through a pointer, it means that the function has read and write permissions to the value pointed to by the pointer. In fact, a pointer is an input and does not want to be modified by the function, and some pointers are output and is expected to be modified by the function. This requires the const keyword to restrict the permission of the function to modify the pointer.

For example, in the following code, the radius of the circle is input to the function through the pointer, and then the circumference and area are calculated through the function and output through the pointer.

#include <stdio.h>

void calculate_perimeter_area(float* radius_p, float* perimeter_p, float* area_p)
{
    
    
    *radius_p= 3;//输入被篡改
    *perimeter_p = 2 * 3.14 * (*radius_p);//2*PI*R
    *area_p = 3.14 * (*radius_p) * (*radius_p);//PI*R^2
}

int main()
{
    
    
    float radius = 2.0;
    float perimeter, area;
    calculate_perimeter_area(&radius, &perimeter, &area);
    printf("radius = %f, perimeter = %f, area = %f \r\n", radius, perimeter, area);
}

The radius of the circle here is also passed to the function through the pointer parameter. But since the pointer is obtained inside the function, the address of radius can be manipulated. If the programmer changes the radius to another number inside the function, the compiler will not report an error, because this is grammatical. The running result is as follows:
insert image description here
Since the input radius is tampered from 2 to 3, the output value is also based on the wrong input.

In order to prevent this situation, as long as the pointer parameter of the function is modified with const, it can be avoided, and the modification is as follows.

#include <stdio.h>

void calculate_perimeter_area(const float* const radius_p, 
                                    float* const perimeter_p, 
                                    float* const area_p)
{
    
    
    *perimeter_p = 2 * 3.14 * (*radius_p);//2*PI*R
    *area_p = 3.14 * (*radius_p) * (*radius_p);//PI*R^2
}

int main()
{
    
    
    float radius = 2.0;
    float perimeter, area;
    calculate_perimeter_area(&radius, &perimeter, &area);
    printf("radius = %f, perimeter = %f, area = %f \r\n", radius, perimeter, area);
}

In the function parameters, two consts are added to the radius_p pointer parameter, indicating that the address pointed to by the pointer parameter and the value in the address cannot be modified, and the function can only read the fixed value in the fixed address. The output perimeter_p and area_p add a const, which is defined as a pointer constant, indicating that the address cannot be modified but the value can be modified. In this way, the calculated value output by the function can only be written to a fixed address.

If there are similar tampering behaviors inside the function, the compiler will report the previous error. In this way, software problems can be found at the stage of compiling the software, and there is no need to wait for abnormal values ​​to appear in the hardware before troubleshooting.

3 Summary

This article lists some common scenarios of pointer usage in C language, but not only those mentioned above. Come back and update when you encounter more complex requirements in the future.

>>Back to personal blog catalog

Guess you like

Origin blog.csdn.net/u013288925/article/details/131344247
Recommended