指针初步理解

一、指针变量

1.1内存和地址

计算机的CPU在处理数据时,需要从内存中读取数据,处理后的数据也会放回内存中,那么内存空间是如何管理的呢?其实是将内存划分一个个单元,每个内存单元的大小为1个字节,每个内存单元有8个比特。每个内存单元都会有一个编号,以便于CPU能够快速找到内存单元。在计算机中,我们把内存单元的编号称之为地址,C语言中给地址起了一个新的名字:地址。

1.2指针变量和地址

每个字节都有唯一的地址,用来和内存中的其他字节相区别。如果内存中有n个字节,就可以把地址看成0~n-1的数。可执行程序由代码和数据两部分组成,数据由原始程序的变量组成。程序中的变量有一个或多个字节组成,将第一个字节的地址称之为变量的地址。在C语言中创建变量其实就是在向内存申请空间,如下图:

上述的代码就是创建了整型变量a,内存中申请4个字节,用于存放整数a,每个字节都有地址。那么如何得到a的地址呢?这里就要使用到取地址操作符{&)。

#include <stdio.h>
int main()
{
   int a = 10;
   printf("%p\n",&a);
   return 0;
}

按照这个例子,会打印出变量a的地址,&a取出的是a所占4个字节中地址较小的字节的地址。虽然整型变量占用4个字节,但只要知道第一个字节地址,就可顺藤摸瓜访问到4个字节的数据。

虽然用数表示地址但是地址的取值范围可能不同于整数的范围,所以一定不能用普通整型变量存储地址。但是,可以用特殊的指针变量存储地址。指针变量也是种变量,这种变量是来存放地址的,存放在指针变量中的值都会理解为地址。

1.3 指针变量的声明和解引用操作符(*)

对指针变量的声明与对普通变量的声明基本一样,唯一的不同就是必须在指针变量名字前放置号,C语言允许使用赋值运算符进行指针的复制,前提是两个指针具有相同的类型。

int a =10;
int * pa = &a; 

pa的左边是int *,*是在说明pa是指针变量,而前面的int是在说明pa指向的是整型(int)类型的对象。

我们将地址保存起来,是要使用的,那应该如何使用呢?在C语言中,我们只要拿到地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,为了实现这种效果,这里就需要使用到解引用操作符(*)。

int main()
{
   int a = 10 ;
   int * pa = &a;
   *pa = 0;
   return 0; 
}

上述代码就使用到了解引用操作符,*pa的意思就是通过pa中存放的地址,找到指向的空间。*pa其实就是a变量了,*pa = 0其实就是将a的值改为0。只要pa指向a,*pa就是a的别名。需要注意的是:不要把间接寻址运算符用于未初始化的指针变量。如果指针变量p 没有初始化,那么试图使用p 的值会导致未定义的行为。

也可以把* 想象成& 的逆运算,对变量使用& 运算符产生指向变量的指针,而对指针使用* 运算符则可以返回到原始变量:“ j   = *&a ;”就等价于 “ j = i;”。

1.4 指针变量的大小

32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或0,那将32根地址线产生的2进制序列当作一个地址,那么一个地址就是32bit位,需要4个字节才能存储。

指针变量用来存放地址的话,那么指针变量的大小就需要4个字节的空间。同理64位机器如果有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。指针变量的大小就是8个字节。

注意指针变量的大小和类型是无关的,只要是指针类型的变量,在相同平台下,大小都是相同的。

1.5 指针变量类型的意义

指针变量的大小和类型无关,那为什么还要有各种各样的指针?其实指针类型是有特殊意义的。

1.5.1 指针的解引用

//代码1
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 int *pi = &n; 
 *pi = 0; 
 return 0;
}

代码1会将n的4个字节全部改成0。 

//代码2
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 *pc = 0;
 return 0;
}

代码2只将n的第一个字节改为0。

通过上述两个代码,char *的指针解引用只能访问一个字节,int *的指针的解引用就能访问四个字节。我们可以看出:指针类型决定了对指针解引用的时候有多大的权限(一次能操作几个字节)。 

 1.5.2 指针+-整数

#include <stdio.h>
int main()
{
 int n = 10;
 char *pc = (char*)&n;
 int *pi = &n;
 
 printf("%p\n", &n);
 printf("%p\n", pc);
 printf("%p\n", pc+1);
 printf("%p\n", pi);
 printf("%p\n", pi+1);
 return 0;
}

代码运行结果如下:

可以看出char * 类型的指针变量+1跳过1个字节,int *类型的指针变量+1跳过4个字节。指针+1其实就是跳过一个指针指向的元素,同时指针也可以-1。指针的类型决定了指针向前或向后走一步有多大。

1.5.3 void*指针 

在指针类型中有一种特殊的类型是void *类型,可以理解为无具体类型的指针(泛型指针),这种指针类型可以用来接受任何任意类型地址,但是void*类型的指针不能直接进行指针的+-整数操作和指针的解引用运算。

void*类型的指针到底有什么应用呢?一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,实现泛型编程的效果。

1.6 const 修饰指针

1.6.1 const修饰变量

变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量。但是给变量加上一些限制,变量便不可以被修改,这就需要用到const。

#include <stdio.h>
int main()
{
 int m = 0;
 m = 20;//m是可以修改的
 const int n = 0;
 n = 20;//n是不能被修改的
 return 0;
}

n是不能被修改的,原因是加上了const修饰,在语法上加了限制,对n进行修改,就不符合语法规则。但是如果使用n的地址,绕过n,就可修改n的值。

#include <stdio.h>
int main()
{
	const int n = 0;
	printf("n = %d\n", n);
	int* p = &n;
	*p = 10;
	printf("n = %d\n", n);
	return 0;
}

运行结果:

可以看到n的值确实被修改了,我们用const修饰n就是为了n的值不被修改,但p拿到了n的地址就能修改n,这就让const失效,这是不合理的,应该是即使p拿到n的地址也不能修改n。

1.6.2 const修饰指针变量

const修饰指针变量,可以放在*的左边,也可以放在*的右边,这两者的意义是不一样的。使用下面这段代码具体分析一下:

#include <stdio.h>
//代码1 - 测试⽆const修饰的情况
void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;
	p = &m;
}

//代码2 - 测试const放在*的左边情况
void test2()
{
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;
	p = &m; 
}
//代码3 - 测试const放在*的右边情况
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20; 
	p = &m; 
}
//代码4 - 测试*的左右两边都有const
void test4()
{
	int n = 10;
	int m = 20;
	int const* const p = &n;
	*p = 20; 
	p = &m; 
}
int main()
{
	//测试⽆const修饰的情况
	test1();
	//测试const放在*的左边情况
	test2();
	//测试const放在*的右边情况
	test3();
	//测试*的左右两边都有const
	test4();
	return 0;
}

可以在编译器中观察到test1() 函数没有语法错误,在test2()函数中,“ *p = 20;”提示了“表达式必须是可修改的左值 ”的错误,说明const修饰的指针指向的内容不能通过指针来改变,但指针变量本身的值可以改变。

test3()函数在“ p = &m ;" 处也出现了表达式必须是可修改的左值 ”的错误,说明此时const限制的是指针变量p,p本身不能被修改,但p指向的内容可以通过指针变量修改。

test4()函数“ *p = 20;”和 “ p = &m ;" 处都出现了表达式必须是可修改的左值 ”的错误,这说明p本身不能被修改,p指向的内容也不能被修改。

总结:(1)const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。

(2)const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

1.7 指针运算

在1.5.2小节中,我们已经介绍过了指针+-整数的运算,指针的基本运算还有两种:指针-指针和指针的关系运算。

1.7.1 指针+-整数

#include <stdio.h>
//指针+- 整数
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));//p+i 这⾥就是指针+整数
	}
	return 0;
}

输出结果:

数组在内存中是连续存放的,只要知道第一个元素的地址,顺藤摸瓜就可以找到后面所有的元素。

1.7.2 指针-指针 

当两个指针相减时,结果为指针之间的距离(用数组元素的个数来度量),如果p 指向a[i] 且q 指向a[j] ,那么p-q 就等于i-j 。需要注意的是,在一个不指向任何数组元素的指针上执行算术运算会导致未定义的行为。此外,只有在两个指针指向同一个数组时,把它们相减才有意义。

1.7.3 指针的关系运算

可以用关系运算符(< 、<= 、> 和>= )和判等运算符(== 和!=)进行指针比较。只有在两个指针指向同一数组时,用关系运算符进行的指针比较才有意义。比较的结果依赖于数组中两个元素的相对位置。

二、 野指针

2.1 野指针的成因

#include <stdio.h>
//代码1
int test1()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
 *p = 20;
 return 0;
}

//代码2
int test2()
{
 int arr[10] = {0};
 int *p = &arr[0];
 int i = 0;
 for(i=0; i<=11; i++)
 {
 //当指针指向的范围超出数组arr的范围时,p就是野指针
 *(p++) = i;
 }
 return 0;
}

//代码3
int* test()
{
 int n = 100;
 return &n;
}
int main()
{
 int*p = test();
 printf("%d\n", *p);
 return 0;
}

野指针的成因只要有三种:指针未初始化(代码1)、指针越界访问(代码2)、指针指向的空间释放(代码3)。

2.2 如何规避野指针

2.2.1 指针初始化

如果知道指针指向哪里就直接赋值地址,如果不知道指针指向哪里,可以给指针赋值NULL。(NULL是C语言中定义的定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。)

2.2.1 小心指针越界

程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超过范围访问,超出了就是越界访问。

2.2.3 指针不再使用时,及时处理指针

当指针变量指向一块区域时,可以通过指针访问这片区域,后期不再使用这个指针访问这片空间的时候,可以把指针置为NULL,同时使用指针之前判断指针是否为NULL。

2.2.4 避免返回局部变量的地址

不要返回局部变量的地址。

三、assert断言

assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行,这个宏常常被称为”断言“。

#include <stdio.h>
#include <assert.h>
int main()
{
	int a = 10;
	int* p = NULL;
	assert(p != NULL);
	printf("%d\n", *p);
	return 0;
}

运行结果: 

如语句"assert(p != NULL) "验证变量p是否等于NULL,如果确实不等于NULL,程序继续运行,否则程序终止运行,给出报错信息提示。

assert()宏接受一个表达式作为参数,如果表达式为真(返回值为非零),assert()不会产生任何作用,程序继续运行。反之,若表达式为假(返回值为 零),assert()就会报错。

使用assert()有几个好处:它不仅能自动标识文件和出问题的行号,还用一种无需要更改代码就能开启或关闭assert()的机制。

如果已经确认程序没有问题,不需要再做断言,就在#include <assert.h>语句的前面,定义一个宏NDEBUG:#define NDEBUG。再重新编译程序后,编译器就会禁用文件中的所有的assert()语句。

assert()的缺点是由于引入了额外的检查,增加了程序的运行时间。一般在Debug中使用assert,在Release版本中禁用assert。

四、指针的使用和传址调用

到目前为止,我们始终没有提及指针在什么时候使用,使用指针的好处又是什么,先看下面这个交换两个整型变量值的Swap1函数:

#include <stdio.h>
void Swap1(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	Swap1(a, b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

运行结果:

可以发现并没有产生交换的效果,这是为什么呢?这是因为在main()函数中创建了a和b两个变量,在调用Swap1()函数时,将a和b传递给Swap1()函数,Swap1()函数内部创建了形参x和y接收a和b的值,但是x和y的地址并不和a和b的地址保持一致,相当于x和y是独立的空间,除了值相等,和a、b毫无关系,在Swap1()函数交换完x和y的值,不会影响到a和b的值。Swap1()函数在使用的时候,是把变量本身直接传递给了函数,这叫做传值调用。总结就是实参传递给形参的时候,形参单独创建一份临时空间接收实参,对形参不会影响到实参。

要解决这个问题,就需要在调用Swap函数时,函数内部直接对main函数内部的a和b进行操作,直接交换a和b,这时候就需要用到指针,将a和b的地址传递给Swap函数,Swap函数内部通过地址间接的操作a和b。使用指针修改上述代码:

#include <stdio.h>
void Swap2(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	Swap2(&a, &b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

运行结果:

可以看到Swap2函数成功完成任务,这里调用Swap2函数的时候是将变量的地址传递给了函数,这种函数的调用方式是:传址调用。传址调用可以让函数和主调函数之间真正建立联系,在被调函数内部修改主调函数的变量;如果被调函数只是需要主调函数的变量的值来实现计算,就可以采用传值调用。

五、指针和数组

5.1 数组名的理解

数组名就是地址,而且是数组首元素的地址,可以通过以下代码验证:

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&arr[0] = %p\n", &arr[0]);
	printf("arr = %p\n", arr);
	return 0;
}

运行结果: 

可以看到数组名和数组首元素的地址是一样的,数组名就是数组首元素(数组第一个元素)的地址。但又会有一个问题:对于上面代码的数组arr,sizeof(arr)的结果是40,如果arr是数组首元素的地址,那么输出的结果应该是4/8,其实数组名就是数组首元素的地址是正确的,但是有两种情况除外:

  • sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的整个数组的字节大小。
  • &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)。

 除了上述这两种情况,任何地方使用数组名,数组名都表示首元素的地址。

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&arr[0] = %p\n", &arr[0]);
	printf("arr = %p\n", arr);
	printf("&arr = %p\n", &arr);
	return 0;
}

执行这段代码,输出结果:

可以看到arr和&arr的结果是一样的,那么它们有什么区别呢?可以比较&arr[0]和&arr[0]+1,它们相差四个字节,比较arr和arr+1,它们之间相差4个字节,这是因为+1就是跳过一个元素,&arr[0]和arr都是首元素的地址。但是&arr和&arr+1相差40个字节,这就是因为&arr是数组的地址,+1操作就是跳过整个数组。

5.2 使用指针访问数组

指针的算术运算允许通过对指针变量进行重复自增来访问数组的元素。下面这个对数组arr中元素输出的程序段说明了这种方法。在这个示例中,指针变量p 初始指向arr[0] ,每次执行循环时对p加i;因此p 先指向arr[0] ,然后指向arr[1] ,依此类推。在p 指向数组arr的最后一个元素后循环终止。

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	//输⼊
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//输⼊
	int* p = arr;
	for (i = 0; i < sz; i++)
	{
		scanf("%d", p + i);
		//scanf("%d", arr+i);//也可以这样写
	}
	//输出
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

数组名arr是数组首元素的地址,可以赋值给p,数组名和p是等价的,那么可不可以使用p[i]访问数组呢?答案是肯定的,可以将*(p+i)换成p[i],本质上p[i]等价于*(p+i),同理arr[i]应该等价于*(arr+i),数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问,其实i[arr]也是可以使用的,原因还是其等价于*(I+arr)。

5.3 一维数组传参的本质

首先需要弄明白一个问题:我们可以直接将数组名传给一个函数后,在这个函数内部求数组元素的个数吗?

#include <stdio.h>
void test(int arr[])
{
	int sz2 = sizeof(arr) / sizeof(arr[0]);
	printf("sz2 = %d\n", sz2);
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz1 = sizeof(arr) / sizeof(arr[0]);
	printf("sz1 = %d\n", sz1);
	test(arr);
	return 0;
}

运行结果:

 

根据运行结果,可以得出答案是不能,数组名是数组首元素地址,在数组传参的时候,传递的是数组名,本质上数组传参传递的是数组首元素的地址,即形参int arr[]也可以写成int *arr。所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。在test函数内部,sizeof(arr)计算的是一个地址的大小,而不是数组的大小。正是因为函数的参数部分的本质是指针,在函数内部是无法计算数组元素的个数的。所以一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

5.4 二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?这就需要二级指针来存放。

#include <stdio.h>
int main()
{
   int a = 1;
   int * pa = &a;
   int ** ppa = &pa;
   return 0;
}

对二级指针的运算有:

  • *ppa 通过对ppa中的地址进行解引用,这样找到的是pa,*ppa其实访问的是pa。
  • **ppa先通过*ppa找到pa,然后对pa进行解引用操作:*pa,那就找到了a。

5.5 指针数组

指针数组是指针还是数组呢?答案是数组,是存放指针的数组,指针数组的每个元素都是用来存放地址(指针)的。可以用指针数组模拟二维数组:

#include <stdio.h>
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
	int* parr[3] = { arr1, arr2, arr3 };
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", parr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

 parr[i]访问的是parr数组,parr[i]找到的是数组元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素。(上述代码模拟的并不是真的二维数组,每一行并不是连续的。)

5.6 字符指针变量

字符指针一般的使用方式为:

char c = 'h';
char * pr =  &ch;
*pr = 'c';

还有另一种使用方式:

const char* pstr = "hello world";
printf("%s\n", pstr);

 这种方式的本质是把字符串首字符的地址放到了pstr中。

#include <stdio.h>
int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	const char* str3 = "hello bit.";
	const char* str4 = "hello bit.";
	if (str1 == str2)
		printf("str1 and str2 are same\n");
	else
		printf("str1 and str2 are not same\n");

	if (str3 == str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");

	return 0;
}

看上述这段代码,运行结果是str1和str2不同,str3和str4相同。这里str3和str4指向的是同一个常量字符串,C语言会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串时,它们实际会指向同一块内存,但是使用相同的常量字符串去初始化不同的数组的时候,就会开辟出不同的内存块。

5.7 数组指针变量

数组指针是一种指针变量,数组指针变量存放的是数组的地址,能够指向数组的指针变量。

数组指针变量的写法是:

int (*p)[10];

p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组,p是一个指针,指向一个数组,叫数组指针。

如果写成:int *p[10],则会因为[]的优先级高于*,而被解读为指针数组。

5.7.1 数组指针变量的初始化

数组指针变量用来存放数组地址,获得数组地址则需要用到前文介绍的&数组名。

int arr[10] = {0};
int(*p)[10] = &arr;

 通过调试可以看到&arr和p的类型是完全一致的。

 5.8 二维数组传参的本质

先来看一下我们常用的将二维数组传给一个函数的写法:

void test(int arr[2][3] , int row ,int col)
{
  
  int i = 0;
  int j = 0;
  for(i=0; i<r; i++)
  {
    for(j=0; j<c; j++)
   {
      printf("%d ", a[i][j]);
   }
    printf("\n");
  }
}

int main()
{

  int arr[2][3] = {{1,2,3},{4,5,6}};
  test(arr,2,3);
  return 0;
}

实参是二维数组,形参也写成二维数组的形式,我们是不是也可以按照一维数组传参那样,形参用指针接收呢?二维数组其实看做成每个元素是一维数组的数组,二维数组的首元素就是第一行,是一个一维数组。根据数组名是数组首元素的地址这一规则,二维数组名应该是第一行的地址,即为一个一维数组的地址,上述代码中,一维数组的类型是int [3],第一行的地址类型是数组指针类型int(*) [5]。所以二维数组传参的本质是传递了地址,传递的是第一行一维数组的地址。即形参也可以写成指针的形式。

#include <stdio.h>
void test(int(*p)[5], int r, int c)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", *(*(p + i) + j));
		}
		printf("\n");
	}
}
int main()
{
	int arr[2][3] = { {1,2,3},{4,5,6} };
	test(arr, 2, 3);
	return 0;
}

六、函数指针

6.1 函数指针变量的创建

函数指针变量应该用来存放函数地址,未来通过地址能够调用函数。函数也是有地址的,函数名就是函数的地址,也可以通过&函数名的方法获得函数的地址。

#include <stdio.h>
void test()
{
	;
}
int main()
{
	printf("test: %p\n", test);
	printf("&test: %p\n", &test);
	return 0;
}

运行结果:

 如果将函数的地址存放起来,就要创建函数指针变量,函数指针变量的写法如下:

int Add(int x, int y)
{
 return x+y;
}
int(*pf1)(int, int) = Add;
int(*pf2)(int x, int y) = &Add;//x和y写上或者省略都是可以的

 

6.2 函数指针变量的使用

通过函数指针调用指针指向的函数:

#include <stdio.h>
int Add(int x, int y)
{
 return x+y;
}
int main()
{
 int(*pf3)(int, int) = Add;
 
 printf("%d\n", (*pf3)(7, 8));
 printf("%d\n", pf3(7, 8));
 return 0;
}

运行结果:

 6.3 typedef关键字

typedef是用来类型重命名的,可以将复杂的类型简单化。例如:

typedef unsigned int unit;

 如果觉得unsigned int 写起来不方便,可以将其改写成unit。

如果是指针类型,也可以重命名,例如将void(*) (int)类型重命名为pf_t,可以这样写:

typedef void(*pf_t)(int);//新的类型名必须在*的右边

6.4 函数指针数组 

将函数的地址存放到一个数组中,那么这个数组就叫函数指针数组,函数指针数组定义如下:

int (*parr[3])()

parr先和[]结合,说明其是数组,然后数组的内容是int* ()类型的函数指针。

6.5 转移表

在C语言中,转移表通常指的是一种使用数组或表来处理状态转移的机制。

我们先实现计算器的加减乘除运算:

#include <stdio.h>
int add(int a, int b)
{
 return a + b;
}
int sub(int a, int b)
{
 return a - b;
}
int mul(int a, int b)
{
 return a * b;
}
int div(int a, int b)
{
 return a / b;
}
int main()
{
 int x, y;
 int input = 1;
 int ret = 0;
 do
 {
 printf("*************************\n");
 printf(" 1:add 2:sub \n");
 printf(" 3:mul 4:div \n");
 printf(" 0:exit \n");
 printf("*************************\n");
 printf("请选择:");
 scanf("%d", &input);
 switch (input)
 {
 case 1:
 printf("输⼊操作数:");
 scanf("%d %d", &x, &y);
 ret = add(x, y);
 printf("ret = %d\n", ret);
 break;
 case 2:
 printf("输⼊操作数:");
 scanf("%d %d", &x, &y);
 ret = sub(x, y);
 printf("ret = %d\n", ret);
 break;
 case 3:
 printf("输⼊操作数:");
 scanf("%d %d", &x, &y);
 ret = mul(x, y);
 printf("ret = %d\n", ret);
 break;
 case 4:
 printf("输⼊操作数:");
 scanf("%d %d", &x, &y);
 ret = div(x, y);
 printf("ret = %d\n", ret);
 break;
 case 0:
 printf("退出程序\n");
 break;
 default:
 printf("选择错误\n");
 break;
 }
 } while (input);
 return 0;
}

不难发现switch语句较多且较为同质化,如果有更多种类的运算,switch语句将会非常长,我们可以使用转移表减少代码的冗余:

#include <stdio.h>
int add(int a, int b)
{
 return a + b;
}
int sub(int a, int b)
{
 return a - b;
}
int mul(int a, int b)
{
 return a*b;
}
int div(int a, int b)
{
 return a / b;
}
int main()
{
 int x, y;
 int input = 1;
 int ret = 0;
 int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
 do
 {
 printf("*************************\n");
 printf(" 1:add 2:sub \n");
 printf(" 3:mul 4:div \n");
 printf(" 0:exit \n");
 printf("*************************\n");
 printf( "请选择:" );
 scanf("%d", &input);
 if ((input <= 4 && input >= 1))
 {
 printf( "输⼊操作数:" );
 scanf( "%d %d", &x, &y);
 ret = (*p[input])(x, y);
 printf( "ret = %d\n", ret);
 }
 else if(input == 0)
 {
 printf("退出计算器\n");
 }
 else
 {
 printf( "输⼊有误\n" ); 
 }
 }while (input);
 return 0;
}

上述代码就用指针数组实现了转移表的功能。

 6.6 回调函数

 回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

我们可以对上文计算器的代码进行另一种改造,输入输出操作是冗余的,只有调用函数的逻辑是有差异的,我们可以把调用的函数的地址以参数的形式传递过去,使用函数指针接收,函数指针指向什么函数就调用什么函数,这样就使用到了回调函数的功能。

//使⽤回到函数改造后
#include <stdio.h>
int add(int a, int b)
{
 return a + b;
}
int sub(int a, int b)
{
 return a - b;
}
int mul(int a, int b)
{
 return a * b;
}
int div(int a, int b)
{
 return a / b;
}
void calc(int(*pf)(int, int))
{
 int ret = 0;
 int x, y;
 printf("输⼊操作数:");
 scanf("%d %d", &x, &y);
 ret = pf(x, y);
 printf("ret = %d\n", ret);
}
int main()
{
 int input = 1;
 do
 {
 
  printf("*************************\n");
  printf(" 1:add 2:sub \n");
  printf(" 3:mul 4:div \n");
  printf("*************************\n");
  printf("请选择:");
  scanf("%d", &input);
  switch (input)
 {
  case 1:
   calc(add);
   break;
  case 2:
   calc(sub);
   break;
  case 3:
   calc(mul);
   break;
  case 4:
   calc(div);
   break;
  case 0:
   printf("退出程序\n");
   break;
  default:
   printf("选择错误\n");
   break;
  }
 } while (input);
 return 0;
}

  • 20
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值