C语言基础:指针的使用

本文结合工作经验,研究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\r\n", p);
    printf("变量a的数值是%d\r\n", a);
    printf("变量b的数值是%d\r\n", *p);
}

首先,定义一个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 \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);

}

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

C语言基础:指针的使用_第1张图片
这是因为前者的函数传参是形参,只是外部传入的参数的复制,交换了数值不影响外部;而后者传入的地址是和外面的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 \r\n", radius, perimeter);

}

通过调用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 \r\n", radius, perimeter, area);
}

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

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

另外,即使是只返回一个参数,也往往不用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 \r\n", circle.Radius, circle.Perimeter, circle.Area);
}

上面的代码中,把圆的半径、周长、面积三个属性定义在同一个结构体类型中。将结构体变量的地址作为参数传给函数,这样只需要传递一个地址变量,函数内部就能获得输入、输出的所有信息。同时,由于只传递一个地址,这个函数只用了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 \r\n", b);
}

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

#include 

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

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 \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);
}

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

利用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 \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);
}

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

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

2.3 空指针

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

#include 

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

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

#include 

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

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

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

C语言基础:指针的使用_第3张图片
但是,同样的代码放到别的编译器中,就不一定报错。譬如通过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 \r\n", circle.Radius, circle.Perimeter, circle.Area);
    }
    else
    {
        printf("Function failed!");
    }    
}

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

2.4 const指针

2.4.1 基本概念

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

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

const int* p = &a;

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

#include 

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

运行代码后,会报错如下:
C语言基础:指针的使用_第4张图片
这里编译器就提示常量无法赋值。但是,指针所指向的地址是可以修改的,例如如下代码。

#include 

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

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

int* const p = &a;

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

#include 

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

运行代码后,同样是会报错。
C语言基础:指针的使用_第5张图片
这表示指针所指向的地址无法被赋值为其他地址。但是,指针所指向的内存地址的值是可以修改的,例如如下代码。

#include 

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

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

const int* const p = &a;

具体就不再举例。

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

这里的圆半径也是通过指针参数传递给函数。但是由于函数内部获得了指针,就可以操作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 \r\n", radius, perimeter, area);
}

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

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

3 总结

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

>>返回个人博客总目录

你可能感兴趣的:(c语言)