当前位置:   article > 正文

C语言基础:指针的使用_c语言指针的用法

c语言指针的用法

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

1 指针的概念

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

#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);
}
  • 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 <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);

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

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

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

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

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

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

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

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

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

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

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

#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);
}
  • 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 <stdio.h>

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

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

#include <stdio.h>

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

2.2.2 使用场景

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

#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);
}
  • 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 <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);
}
  • 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 <stdio.h>

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

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

#include <stdio.h>

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

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

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

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

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

#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!");
    }    
}

  • 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

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

另外,在编程的时候可以故意使用空指针并判断,从而跳过一些不需要处理的元素,例如下面的代码,定义一个具有4个元素的指针数组,其中前2个元素赋值为结构体变量的指针,后两个为空指针。

#include <stdio.h>
#include <string.h>

#define CIRCLE_NUMBER 4

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

void calculate_perimeter_area(Circle_Type* circle_p)
{
	if (NULL != 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_multiple_circle(Circle_Type* circle_vp[CIRCLE_NUMBER])
{
	for (int i = 0; i < CIRCLE_NUMBER; i++) {
		calculate_perimeter_area(circle_vp[i]);
	}

	for (int i = 0; i < CIRCLE_NUMBER; i++) {
		if (NULL != circle_vp[i]) {
			printf("Circle %d , radius = %f, perimeter = %f, area = %f \r\n", i, circle_vp[i]->Radius, circle_vp[i]->Perimeter, circle_vp[i]->Area);
		}
	}
}

int main()
{
	Circle_Type circle1;
	Circle_Type circle2;
	Circle_Type* circle_vp[CIRCLE_NUMBER];//定义指针数组
	memset(circle_vp, 0, CIRCLE_NUMBER * sizeof(Circle_Type*));

	circle1.Radius = 2.0F;
	circle2.Radius = 3.0F;
	circle_vp[0] = &circle1;//指针数组赋值
	circle_vp[1] = &circle2;
	
	calculate_multiple_circle(circle_vp);
}
  • 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

这样的做法可以灵活地输入不同个数的元素(只要少于4个),并输出对应的计算结果。在汽车的目标检测中,经常每一帧检测到不同数量的目标,就可以使用这种方法处理。

2.4 const指针

2.4.1 基本概念

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

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

const int* p = &a;
  • 1

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

#include <stdio.h>

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

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

#include <stdio.h>

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 <stdio.h>

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

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

#include <stdio.h>

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

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

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

#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);
}
  • 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语言中指针使用的一些常见场景,但不仅仅是上文提到的这些。在今后遇到更复杂的需求时再回来更新。

>>返回个人博客总目录

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/784029
推荐阅读
相关标签
  

闽ICP备14008679号