首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

C语言基础:指针的使用

  • 23-09-22 01:23
  • 2687
  • 5512
blog.csdn.net

本文结合工作经验,研究C语言中指针的用法。

文章目录

  • 1 指针的概念
  • 2 用法与使用场景
    • 2.1 函数的指针参数
      • 2.1.1 基本概念
      • 2.1.2 使用场景1-函数返回多个值
      • 2.1.3 使用场景2-减少函数参数
    • 2.2 void*指针
      • 2.2.1 基本概念
      • 2.2.2 使用场景
    • 2.3 空指针
    • 2.4 const指针
      • 2.4.1 基本概念
      • 2.4.2 使用场景
  • 3 总结

1 指针的概念

指针是C语言的精髓,用于存放变量的地址。通过指针可以间接地访问该地址中所存储变量的数值。对于指针,首先需要理解&和*两个运算符的含义,举例如下。

#include 

int main()
{
    int a = 1;
    int* p = &a;
    int b = *p;
    printf("变量a的地址是%p
", p);
    printf("变量a的数值是%d
", a);
    printf("变量b的数值是%d
", *p);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

首先,定义一个int类型的变量a,同时赋值为1;接着定义一个指针p,赋值为变量a的地址(通过&运算符取地址);然后分别打印出变量a的地址p以及变量a的数值,接着打印变量b的数值,通过*运算符获取p地址中的变量。

上面是个非常基础的例子,是大学一年级学生就应该掌握的。博主根据工作经验,总结指针在汽车软件C语言开发中运用的场景。

2 用法与使用场景

2.1 函数的指针参数

2.1.1 基本概念

大学就学过,C语言函数的参数是形参。在函数内部,无论形参如何改变,都无法改变函数外的实参。典型的例子是通过函数交换a和b的数值,如下。

#include 

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 
", a, b);
    swap(a, b);
    printf("After swap(a, b) : a = %d, b = %d 
", a, b);
    swap_pointer(&a, &b);
    printf("After swap_pointer(a, b) : a = %d, b = %d 
", a, b);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

代码中,定义了swap和swap_pointer两个函数。前者传参是int类型的变量,在函数内部交换a和b;后者传参是指针参数,在函数内部通过地址解引用的方式交换数值。运行代码后,如下图:

在这里插入图片描述
这是因为前者的函数传参是形参,只是外部传入的参数的复制,交换了数值不影响外部;而后者传入的地址是和外面的a,b的地址是一样的,所以直接操作地址对应的内存空间就能影响到函数外部。

2.1.2 使用场景1-函数返回多个值

指针作为函数参数比较常见于函数需要输出多个返回值的场景。

函数只有一个输出时,可以用return返回值的方式。例如下面的代码,通过将圆的半径作为参数传递给函数,函数经过计算返回圆的周长。

#include 

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 
", radius, perimeter);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

通过调用calculate_perimeter()函数,从他的返回值获取了半径对应的周长。但是如果需求更加复杂一点,希望通过半径计算圆的周长和面积,如果还是通过返回值的形式就必须设计两个函数,如下。

#include 

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 
", radius, perimeter, area);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

通过指针传参的方式,就可以设计一个函数,返回两个值,如下。

#include 

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 
", radius, perimeter, area);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

另外,即使是只返回一个参数,也往往不用return的方式返回。这是因为,返回值用来作为函数是否运行成功的标志。

2.1.3 使用场景2-减少函数参数

很多企业规范要求C语言的函数参数尽量少一些,例如一个函数的参数少于5个。这样的要求通常是为了代码的可读性,以及节省栈空间的使用。

如果一个函数的输入确实很多,可以考虑把他们打包成结构体,再将结构体变量的指针作为函数参数。例如,上面的计算圆的半径、周长的函数可以改造一下。

#include 

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 
", circle.Radius, circle.Perimeter, circle.Area);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

上面的代码中,把圆的半径、周长、面积三个属性定义在同一个结构体类型中。将结构体变量的地址作为参数传给函数,这样只需要传递一个地址变量,函数内部就能获得输入、输出的所有信息。同时,由于只传递一个地址,这个函数只用了4个字节的栈空间。而传递三个float类型的变量,就需要12个字节。

2.2 void*指针

2.2.1 基本概念

void* 指针是一种没有具体类型的指针。int类型的指针和void类型的指针都存放了一个地址,但是由于int类型指针指到它所指向的内存空间是int类型,就可以通过解引用得到该地址处4个字节的空间中的变量值。而void* 指针不知道这段地址占了几个字节,就取不出来变量数值。看一下下面这段代码:

#include 

int main()
{
	int a = 1;
	void* p = (void*)&a;
	int b = *p;
	printf("b = %d 
", b);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

代码中,定义void*定义指针p,并且将变量a的地址赋值给p。然后又试图通过解引用的方式,把p指向的内存空间的变量数值赋值给b。运行代码就会报错如下:
在这里插入图片描述
因为指针变量p中只包含了地址,不知道具体类型,就无法从地址中获得数值。正确的做法是将void*指针先进行强制类型转换,再解引用。

#include 

int main()
{
	int a = 1;
	void* p = (void*)&a;
	int b = *(int*)p;
	printf("b = %d 
", b);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2.2.2 使用场景

void*类型指针用的时候还需要强制类型转换,看起来十分麻烦,但也有他所使用的场景。在函数设计的时候,需要明确参数的数据类型,这就导致了很多时候函数难以统一化和平台化。例如,还是需要设计用来计算周长和面积的函数,但是输入的几何图形是圆形和矩形两种。由于两种几何图形分别对应两个结构体类型,就必须设计两个函数分别用于计算周长和面积。如下代码。

#include 

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 
", 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 
", rectangle.Length, rectangle.Width, rectangle.Perimeter, rectangle.Area);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

代码中,由于结构体类型不同,就必须设计两个函数来输入不同的参数,分别处理两种几何图形的计算。

利用void*类型指针,就可以设计为一个函数。代码如下:

#include 

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 
", 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 
", rectangle.Length, rectangle.Width, rectangle.Perimeter, rectangle.Area);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

通过calculate_geometry函数的第二个参数,可以判断出第一个参数的void*指针是由圆类型还是矩形类型转换来的,从而在函数内部将void*指针强制类型转换回原来的类型,再用进行对应的计算。

calculate_geometry函数使用了void*类型参数,可以称之为弱类型参数。明确定义了类型的参数,例如float和int等,称之为强类型参数。对于上面这种函数接口需要通用的场景,就可以使用弱类型参数。

2.3 空指针

在定义一个指针时,如果不立即赋值,指针就会指向一个随机的地址。比较好的做法是应该在定义指针的时候就赋值为空,在C语言中就是NULL,如下。

#include 

int main()
{
    int* p = NULL;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这样保证了指针的地址是0,但是指针还是不能解引用,因为程序员应该给指针真正赋值为有意义的地址,才能从内存的地址中取出变量。如果对空指针解引用,还是会报错,例如下面的代码。

#include 

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

    printf("a = %d 
", a);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在visual studio中运行后,会报出错误。

在这里插入图片描述
但是,同样的代码放到别的编译器中,就不一定报错。譬如通过Hightec或Tasking编译器,为嵌入式硬件编译代码,可以成功地生成elf文件。但是软件刷写到嵌入式控制器中,硬件运行就会卡死,需要花费大量的精力在硬件上debug才能定位到这个问题。

在代码编写的时候就应该注意校验指针是否为空指针。例如,把上面的计算圆形的周长面积的函数可以再做一个空指针校验。

#include 

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 
", circle.Radius, circle.Perimeter, circle.Area);
    }
    else
    {
        printf("Function failed!");
    }    
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

这样设计函数,就可以通过返回值提示函数的调用者,函数是否调用失败,从而排查出参数传递了空指针。

2.4 const指针

2.4.1 基本概念

const关键字修饰变量时,表示这个变量的数值不能改变并且在被定义的时候需要立即赋值,后面就不可改变了。const关键字修饰指针的时候,根据const所处的位置,指针的特点有所不同。

1)如下代码是常量指针,在定义指针的时候先写const,再写int*。

const int* p = &a;
  • 1

由于const是在int*之前的,所以这里的const的含义是指针所指向的内存的值是常量,这个值不能被修改。例如下面代码,试图修改常量指针所指向的值,就会报错。

#include 

int main()
{
    int a = 10;
    const int* p = &a;
    *p = 20;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

运行代码后,会报错如下:
在这里插入图片描述
这里编译器就提示常量无法赋值。但是,指针所指向的地址是可以修改的,例如如下代码。

#include 

int main()
{
    int a = 10;
    const int* p = &a;
    int b = 20;
    p = &b;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2)如下代码是指针常量,在定义指针的时候先写int*,再写const。

int* const p = &a;
  • 1

由于int*是在const之前的,所以这里的const的含义是指针所指向的地址是常量,不能改变它所指向的地址。例如下面代码,试图修改指针常量所指向的地址,就会报错。

#include 

int main()
{
    int a = 10;
    int* const p = &a;
    int b = 20;
    p = &b;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

运行代码后,同样是会报错。
在这里插入图片描述
这表示指针所指向的地址无法被赋值为其他地址。但是,指针所指向的内存地址的值是可以修改的,例如如下代码。

#include 

int main()
{
    int a = 10;
    int* const p = &a;
    int b = 20;
    *p = b;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3)将以上两个const融合,就成为了指向常量的常指针,就意味着地址和值都不可以被改变。

const int* const p = &a;
  • 1

具体就不再举例。

2.4.2 使用场景

const修饰指针的主要使用场景还是在函数的参数为指针的时候。当函数参数通过指针传参,就意味着函数内部对指针指向的值有读和写的权限。实际上某个指针是输入,不希望被函数修改,某些指针是输出,希望被函数修改,这就需要通过const关键字来约束函数修改指针的权限。

例如下面的代码,将圆的半径通过指针输入给函数,再通过函数计算出周长和面积通过指针输出。

#include 

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 
", radius, perimeter, area);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这里的圆半径也是通过指针参数传递给函数。但是由于函数内部获得了指针,就可以操作radius的地址。如果程序员在函数内部将radius篡改成别的数字,编译器也是不会报错的,因为这是符合语法规范的。运行结果如下:
在这里插入图片描述
由于输入的radius从2篡改到3,输出的值也是基于错误的输入得出的。

为防止这种情况,只要将函数的指针参数加上const修饰,就可以避免,修改如下。

#include 

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 
", radius, perimeter, area);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

函数参数中,为radius_p指针参数加上了两个const,表示该指针参数所指向的地址,以及地址里的值都不能被修改掉,函数内部只能读取固定地址里的固定数值。输出的perimeter_p和area_p加了一个const,定义为指针常量,表示地址不能被修改但是值可以被修改。这样,函数输出的计算值只能写入固定的地址中。

如果函数内部还有类似的篡改行为,编译器就会报之前的错误。这样,就可以在编译软件的阶段发现软件问题,不必等到硬件中出现异常值再去排查。

3 总结

本文中列举了C语言中指针使用的一些常见场景,但不仅仅是上文提到的这些。在今后遇到更复杂的需求时再回来更新。

>>返回个人博客总目录

文章知识点与官方知识档案匹配,可进一步学习相关知识
C技能树指针指针与数组160872 人正在系统学习中
注:本文转载自blog.csdn.net的chhttty的文章"https://blog.csdn.net/u013288925/article/details/131344247"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top