栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

深度剖析C语言指针

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

深度剖析C语言指针

深度剖析语言指针
  • 一.指针基础知识:
    • 1.指针的含义
    • 2.取地址符&:
    • 3.访问地址上的的变量:
    • 4.指针的大小:
    • 5.字符指针
    • 6.野指针:
    • 7.二级指针:
  • 二.指针类型:
    • 1.指针类型的意义
    • 2.指针的类型转换
  • 三.指针的运算
    • 1.指针加减整数:
    • 2.指针减指针:
    • 3.指针的关系运算:
  • 四.指针的应用
    • 1.在某函数自己的变量空间中修改其它函数的变量:
    • 2.传结构:
    • 3.对数组进行操作:
    • 4.函数返回不止一个结果:
    • 5. 动态申请内存:
  • 五.指针与数组的关系
    • 1.指针数组:
    • 2.数组指针
    • 3.[],*运算符:
    • 4.数组变量实际上是const的指针:
    • 5.函数参数表中的数组参数与指针参数:
    • 6.我们可以通过指针来访问数组:
    • 7.数组遍历的四种方式:
  • 六.函数与指针之间的关系
    • 1.函数指针
    • 2.函数指针数组
    • 3.指向函数指针数组的指针
    • 4.回调函数

一.指针基础知识: 1.指针的含义

一个指针类型的变量就是保存地址的变量,指针变量的值是变量的地址:

int i;
int* p = &i;
int *p = &i;//与上述表达是一样的意思

*通常靠着类型,可以将其理解为指针类型,而下面这种情况,*通常靠着变量名,用于告诉我们这是不是指针变量。

typedef int* pint;
#define PINT int*

int *p,q//p是指针变量,而q只是一个普通变量
int *p,*q;//才表示p,q都是指针变量

pint pa, pb;
//这里的pint是一个类型,这表示pa和pb都是pint类型的变量,也就是pa和pb都是int类型的指针变量

PINT pa, pb;
//而这里的PINT不是一个类型,它只是一个宏,编译预处理的时候会替换为int*
//也就是说PINT pa,pb实际上就是int* pa,pb,那么pa是指针变量,pb是普通变量
void f(int *p);

int i = 0;
f(&i);

注:

  • int表示指针变量p所指向的变量类型是int

  • *表示p是一个指针变量

  • “ * ” 是加给p而非int,即*p是一个int类型的指针变
    量,而非P是一个int * 类型的变量

  • 参数是一个指针,则我们在调用参数的时候就应该传入变量的地址

  • 有的变量是2字节,4字节,8字节,指针保存的是变量首字节的地址

  • 要注意不能写出以下代码:

    指针变量p保存text函数的返回值,即保存变量a的地址,然后输出指针所指向的变量的值,上面代码看似没有问题,实际上问题很大,在text函数完成调用之后,虽然变量a仍然存在于内存中,但是对于程序而言,变量a已经消亡了,因此把一个对于程序而言已经消亡的变量的地址赋值给一个指针变量,这本身就是很大的问题。

2.取地址符&:

f(“%d”,&i)里的& 获得变量的地址,它的操作数必须是变量,地址的类型是否与int相同取决于编译器,所以我们通常输出地址需要用下面的输出格式

int i;
printf("%p,&i);
int a[];
printf("%pn",a);//0xbff8dd44
printf("%pn",&a[0]);//0xbff8dd44
printf("%pn",&a[1]);//0xbff8dd48

a==&a[0]//它们都表示数组中第一个元素的地址
3.访问地址上的的变量:

*是一个单目运算符,用来访问指针变量保存的地址所对应的变量,即访问指针变量所指向的变量,可以作右值,也可以作左值

int k = *p//访问指针变量p保存的地址所对应的变量,即访问指针p所指向的变量

*p = k+1;//将k+1的值赋值给指针变量p保存的地址所对应的变量,即赋值给指针变量p所指向的变量
void f(int *p);
void g(int k);

int main(void)
{
	int i =6;
	f(&i);
	//p=0x234353
	/
	g(i);//k=26
	
	return 0;
}
void f(int *p)
{
	printf("p=%pn",p);
	printf("*p=%dn",*p);
	*p =26;
	
}
void g(int k)
{
	printf("k=%dn",k);
}


4.指针的大小:

内存是电脑上特别重要的存储器,计算机中所有程序的运行都是在内存中进行的 。所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。

对于32位的机器,假设有32根地址线,那么在每次寻址时每根地址线产生的电信号(正电/负电,1或者0)的集合就表示一个地址,那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
…
11111111 11111111 11111111 11111111

共有32根地址线,故一共有2的32次方个地址。

每个地址标识一个字节,那我们就可以给 (2^32Byte = 2^32/1024KB =
2^32 /1024/1024MB=2^32/1024/1024/1024GB = 4GB),共 4G的空间进行编址。
同样的方法,那64位机器,假设有64根地址线,那能编址多大空间,自己计算。

因此,在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址,即在64位机器上指针大小为8字节。

注: 不是说64位系统,指针就一定占8个字节,关键是要按照64位方式编译,比如我的电脑是64位操作系统,但是按32位方式编译,那指针就是4字节的。

#define _CRT_SECURE_NO_WARNINGS 1 
#include 
int main()
{
	printf("%dn", sizeof(char*));
	printf("%dn", sizeof(short*));
	printf("%dn", sizeof(int*));
	printf("%dn", sizeof(long*));
	printf("%dn", sizeof(long long*));
	printf("%dn", sizeof(float*));
	printf("%dn", sizeof(double*));
	return 0;
}

在64位编译环境下,指针大小都是8字节,而如果在32位编译环境下,则指针大小都是4字节。

5.字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;
一般使用:

int main()
{
  char ch = 'w';
  char *pc = &ch;
  *pc = 'w';
  return 0;
}

当然还有一种使用方式如下:

int main()
{
  char* p = "abcdef";
  //这里是把一个字符串放到pstr指针变量里了吗?
  printf("%sn", p);
  return 0;
}

代码 char* p = “abcdef.”; 特别容易让同学以为是把字符串 abcdef 放到字符指针 p 里了,但是本质是把字符串 abcdef 首字符的地址放到了p中,那这是为什么呢?

双引号引起来的abcdef是一个常量字符串,本质是一个字符数组,赋给一个指针,相当于把一个数组的首地址赋给指针,即把第一个元素a的地址赋值给了指针。

根据上面的讲解,我们来看看下面这道面试题:

#include 
int main()
{
  char str1[] = "hello bit.";
  char str2[] = "hello bit.";
  char *str3 = "hello bit.";
  char *str4 = "hello bit.";
  if(str1 ==str2)
	 printf("str1 and str2 are samen");
  else
	 printf("str1 and str2 are not samen");
  
  if(str3 ==str4)
	 printf("str3 and str4 are samen");
  else
	 printf("str3 and str4 are not samen");
  
  return 0;
}

输出结果如下:

那为什么会这样呢?

C/C++会把常量字符串存储到单独的一个内存区域,字符串常量在内存中只会存储一份,所以当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是str1和str2是两个数组,它们在被定义的时候,会开辟出不同的内存空间,而初始化是将右边常量字符串的内容拷贝进来,所以他们是两个空间,只是内容相同。所以str1和str2不同,str3和str4相同。

另外值得一提的是,存储在字符串常量池中的字符串是不可以被修改的,如果你强行通过指针修改,虽然编译的时候不会报错,但运行起来不会有任何结果,调试的时候,它也会对此发出警告。

比如下面这段代码,强行修改字符串常量:

#include
int main()
{
	char* p = "abcdef";
	*p = 'w';//试图通过指针p修改字符串常量

	printf("%cn", *p);//修改完后输出

	return 0;
}

我们将上面代码运行起来看一下:

如图所示,运行后没有任何结果。

我们再调试一下看看原因。

如图所示,调试时候的原因清楚地告诉了我们,写入访问权限冲突。

因为前面说过字符串常量不可以修改,而我们这里强行通过指针修改,所以调试的时候它会报一个异常:写入访问权限冲突。

为了避免有人想通过指针对不可修改的字符串常量进行修改,我们可以使用const关键字:

const char* p = "abcdef";

这样的话就无法通过指针修改其所指向的内容了。

6.野指针:

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),即野指针指向未分配或者已经释放的内存地址。

野指针成因:

  • 指针未初始化:
#include 
int main()
{
	int *p;//局部变量指针未初始化,默认为随机值
  *p = 20;
  
	return 0;
}
  • 指针越界访问:
#include 
int main()
{
  int arr[10] = {0};
  int *p = arr;
  int i = 0;
  for(i=0; i<=11; i++)
 {
    //当指针指向的范围超出数组arr的范围时,p就是野指针
    *(p++) = i;
 }
  return 0;
}
  • 指针指向的空间释放

如何规避野指针:

  1. 指针初始化
  2. 小心指针越界
  3. 指针不想用的时候,释放空间,即置NULL
  4. 指针使用之前检查有效性
#include 
int main()
{
  int *p = NULL;
  //....
  int a = 10;
  p = &a;
  if(p != NULL)
 {
    *p = 20;
 }
  return 0;
}
7.二级指针:

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

对于二级指针的运算有:
*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa :

int b = 20;
*ppa = &b;//等价于 pa = &b;

**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a :

**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
二.指针类型: 1.指针类型的意义

实际上指针无论是什么类型,都可以存放任何类型变量的地址,以32位机器为例:任何类型的指针大小都为4字节,而任何类型变量的地址都是4字节,因此4字节大小的指针完全可以存放任何类型变量的地址,那么指针类型还有什么意义呢?

  • 指针的类型决定了指针每次加一或者减一后指针移动的字节数:

    如图所示char类型的指针p1和int类型的指针p2都是保存变量a的地址,但是p1+1,指针p1只向后移动了一个char,即值移动一个字节。p2+1,指针向后移动一个int,即移动了4个字节。因此指针的类型决定了指针每次加一或者减一指针向后/向前移动的字节数。

注:

我们需要使用指针操作数组中的元素时,通常指针需要和数组元素类型保持一致,这样指针加一减一就是跳过一个元素。

  • 指针的类型决定了,对指针解引用的时候有多大的访问权限(能操作几个字节)。

比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节:

int * 的指针pi每次能访问4个字节,因此pi = 0将4个字节的数据全改为了0,也就是把整个变量a改为了0,而char 的指针pc每次只能访问一个字节,因此*pc = 0只是将第一个字节的数据改为了0(从低位开始访问)。

因此对于某类型的变量,如果我们想每次1字节1字节的访问,那么我们可以用char类型的指针。

2.指针的类型转换

指针也可以转换类型,int * p = &i; void * q = (void *)p; 这并没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看它所指的变量,我不再当你是int啦,我认为你就是个void!

指向不同类型的指针是不能直接互相赋值的,这是为了避免用错指针,虽然编译器可以通过,也可以运行,但我们在赋值的时候,尽量将其强制类型转化为相同的类型:

int a = 10;
char* ch = (char*)&a;
三.指针的运算 1.指针加减整数:

指针的类型决定了指针每次加一减一移动的字节数,char类型的指针每次加一减一向后/向前移动1字节,每加2减2向后/向前移动2个字节,每加n减n向后/向前移动n个字节。

int类型的指针每加1减1向后/向前移动4字节,每加2减2向后/向前移动8字节,每加n向后移动n个步长,即4n个字节

short,long,float,double也和上述一样推理

同理。这些算术运算可以对指针做,给指针加,减一个整数(+,+=,-,-=),递增递减(++,–)。

当然如果指针不是指向一片连续分配的空间,如数组,则这种加1运算就没有意义,如果一个int类型的指针int类型的数组,那么指针加1即表示数组的下一个元素。

注:
*p++表示什么意思?

自增运算符的优先级要高于解引用运算符的优先级,因此*p++中会先执行p++,但是由于这是后++,故要执行语句之后再+1,也就是先将p解引用之后再自增1,看下面代码:

#include

int main()
{
	int arr[] = { 1,2,3 };
	int* p = arr;

	printf("%dn", *p++);
	return 0;
}

这里的*p++就是指先将p解引用输出之后再+1,所以输出的是1.
值得一提的是,*p++和 *(p++)是一个意思。

2.指针减指针:

指针相减表示两个指针之间有几个这样类型的元素(包头不包尾):

#include
int main()
{
	int a[] = { 1,2,3,4,5 };
	int* p1 = &a[0];
	int* p2 = &a[4];
	printf("p1-p=%dn", p2 - p1);//4
	return 0;
}

可用此性质来求字符串的长度:

int my_strlen(char *s)
{
   char *p = s;
   while(*p != '' )
       p++;
   return p-s;
}

指针减指针的前提是这两个指针指向同一片空间

3.指针的关系运算:

指针的关系运算是指地址之间大小的比较,<,<=,==,>,>=,!=都可以对指针做,比较它们在内存中的地址,数组中的单元的地址肯定是线性递增的。

将数组初始化为0:

for(vp = &values[N_VALUES]; vp > &values[0];)
{
  *--vp = 0;
}

注: 这里的指针vp并没有越界,因为指针vp只是指向它,并没有访问它,因此指针vp并没有越界。

将上述代码简化为:

for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
  *vp = 0;
}

实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

四.指针的应用 1.在某函数自己的变量空间中修改其它函数的变量:

为什么上述代码并没有成功交换a和b的值呢?

每个函数都有自己的变量空间,调用该函数的时候相当于进入了该函数的变量空间,该函数调用结束,也就相当于离开了该函数的变量空间。

在调用函数swap的时候,进入swap函数的变量空间,传参只是将a和b的值传给了swap函数变量空间中的x和y,swap函数只是交换了自己变量空间中x和y的值,并没有对main函数中的变量a和b进行任何操作。

那么我们需要在swap函数中交换main函数中的变量a和b的值该怎么办呢?

我们只能将指针变量作为调用函数的参数,然后将main函数中的变量地址传过去,只有这样在调用函数中就可以通过指针直接对main函数中的变量进行操作。

因此如果我们需要在某个函数对另一个函数变量空间中的变量进行操作,我们需要将指针作为该函数的参数,将另一个函数变量的地址传递给该函数,然后在该函数中通过指针直接对另一个函数的变量进行操作。

2.传结构:

比如你需要传入一个变量,该变量的大小远超过它的地址,此时你可以传入它的地址来表示该变量,这也是一种传结构的常用方法。

3.对数组进行操作:

在函数参数表中需要数组参数,用指针参数代替数组参数,然后将数组变量名(首元素地址)赋值给指针变量,即可用指针操作数组

以下四种操作都是正确的

int a[10]={1,2,3);
int *ar = a;
int sum(int *ar,int n);
int sum(int *,int);
int sum(int ar[],int n);
int sum(int [],int);
4.函数返回不止一个结果:

把这些结果存在数组里面,然后用指针指向该数组首元素,最后返回该指针即可,这样的话满足单一出口原则

5. 动态申请内存:

动态内存分配:

#include 
#include //使用malloc这个函数必须包含这个头文件
int main(void)
{
	int number;
	int *a;
	int i;
	printf("输入数量:");
	scanf("%d",&number);
	a = (int*)malloc(number*sizeof(int));
	//向malloc申请的空间大小是以字节为单位的
	//返回一个指向一片内存空间的void型指针,因此要强制转化为自己需要的类型
	//如果申请失败则返回0,或者叫做NULL
	for(i=0;i
		scanf("%d",&a[i]);
	}
	for(i = number-1;i>=0;i--){
		printf("%d",a[i]);
	}
	free(a);//用完之后需要主动释放这片申请的空间,只能还申请来的空间的首地址
	return 0;
}

//free只能还用malloc申请来的空间的首地址,但是
free(NULL)//ok
//如果你没有用malloc,或者说malloc申请失败,此时你
free(p);//ok

申请了没free,长时间会导致内存空间逐渐下降

注:

相邻两次malloc得到的空间是否是连续的呢?

相邻两次malloc得到的空间是非连续的,这样可以尽可能利用内存,但是单次分配是连续的,每次分配都会按你要求的大小分配一片连续空间,如果没有那么大的空间就会分配失败。

五.指针与数组的关系 1.指针数组:

指针数组是存放指针的数组

例1:

int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* arr[] = {arr1, arr2, arr3};

arr是一个整型指针类型的数组,有3个元素,每个元素是一个整形指针。

arr[2][4]指的就是arr3[4],也就是7。

注:

  • int (*arr3)[5]定义了一个数组指针,该指针指向存储5个整形元素的数组。
  • 指针数组变量名也是指针(地址),它保存的是首元素地址,也就是一个一级指针的地址,所以指针数组变量名是二级指针。

例2:

char* arr[] = { "abcdef", "qwer", "zhangsan" };

该数组存储char类型的指针变量,第一个char指针保存的是abcdef中a的地址,第二个char指针指向的是qwer中q的地址,第三个char指针指向的是zhangsan中z的地址。

2.数组指针

顾名思义,数组指针实际上就是指针,我们已经熟悉: 整形指针: int * pint; 是指向整形数据的指针。 浮点型指针: float * pf; 是指向浮点型数据的指针。那数组指针就是指向数组的指针。

数组指针的表示: int (* p)[10];

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

注:

[]的优先级要高于* 号的,所以必须加上()来保证p先和*结合,而且如果不加括号,也就是int *p1[10],这表示p1是一个数组,数组中存储的是10个int类型的指针。

那数组指针是怎么使用的呢?

我们可以通过数组指针来访问二维数组:

在此之前我们需要了解一个概念:

二维数组的数组名表示二维数组的首元素地址,二维数组的首元素就是第一行数组,所以二维数组的首元素地址就是第一行数组的地址。

如果用指针变量p来保存二维数组的地址:

arr[3][5] = {0);
int(*p)[5] = &arr

p保存的是二维数组首元素的地址,即第一行数组的地址,因为指针变量p是int[5]类型的,所以p+1相当于跳过一个int[5],也就是p+1直接跳过一行数组,指向第二行数组。

那么p+i就是指向第i行数组,相当于第i行的数组地址,*(p+i) 相当于了第i行的数组,也相当于第i行的数组名,数组名表示首元素的地址,其实也是第i行第一个元素的地址。

现在我们来看看数组指针的使用:

void print(int(*p)[5], int r, int c)
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{
			printf("%d ", *(*(p + i) + j));
			//               p[i][j]
		}
		printf("n");
	}
}

我们可以通过数组指针来访问二维数组。

学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:

int arr[5];//整型数组
int *parr1[10];//指针数组
int (*parr2)[10];//数组指针
int (*parr3[10])[5];//数组指针类型的数组,即他是一个数组,存储的元素是数组指针类型的
3.[],*运算符:
  • []运算符可以对数组做,也可以对指针做:

  • *运算符可以对指针做,也可以对数组做:
*a//表示首元素 
4.数组变量实际上是const的指针:
  • 数组变量是const的指针,所以不能被赋值:
int a[]--->int* const a;
5.函数参数表中的数组参数与指针参数:

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

一维数组传参

根据调用函数所传的实参,判断函数的形参是否正确。

#include 
void test(int arr[])//因为要传入的就是一个数组,所以把数组作为函数参数是没有问题的。
{}
{}
void test(int arr[10])//同上,因为传入数组本质上传入的是数组首元素的地址,然后在调用函数中通过地址对原数组进行操作,这里压根不会创建数组,所以这里数组的大小可给可不给,就算写错都没有关系。
{}
void test(int *arr)//传入数组的本质就是传入数组首元素的地址,因此用指针来接收数组首元素的地址,完全ok
{}
void test2(int *arr[20])//同上,传入的是一个数组,所以数组作为函数参数是没有问题的,且数组的大小也是可给可不给,写错都没关系
{}
void test2(int **arr)//这也是可以的,因为二级指针用来保存一级指针的地址,传入数组的本质是传入数组首元素的地址,首元素是一级指针,那传入的就是一个一级指针的地址,用二级指针来接收完全ok。
{}

int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}

二维数组传参

void test(int arr[3][5])//传入的是一个3行5列的二维数组,我将3行5列的数组作为形参完全没问题
{}
void test(int arr[][])//这就不行了,因为二维数组作为形参,只能省略行,不能省略列
{}
void test(int arr[][5])//传入的是一个二维数组,所以我将一个知道列数的二维数组作为函数参数完全没有问题
{}
void test(int *arr)//不行,传入二维数组的本质是传入二维数组首元素的地址,即传入的是第一行数组的地址,故要用数组指针来接收数组地址
{}
void test(int* arr[5])//不行,同上,要用数组指针来接收,而这里是指针数组
{}
void test(int (*arr)[5])//这是一个数组指针,它指向一个数组,这个数组有5个int,用它来接收传进来的第一行数组的地址完全没问题
{}
void test(int **arr)//这里不行,因为二级指针保存的是一级指针的地址,这里传入的实际上是数组的地址,故要用数组指针才行
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
6.我们可以通过指针来访问数组:
#include 
int main()
{
  int arr[] = {1,2,3,4,5,6,7,8,9,0};
  int *p = arr; //指针存放数组首元素的地址
  int sz = sizeof(arr)/sizeof(arr[0]);
  for(i=0; i
    printf("&arr[%d] = %p  <====> p+%d = %pn", i, &arr[i], i, p+i);
 }
  return 0;
}

所以 p+i,实际上就是arr+i, 其实计算的是数组 arr 下标为i的地址。 那我们就可以直接通过指针来访问数组。如下,遍历数组中的元素:

int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	int *p = arr; //指针存放数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	
	for (i = 0; i
	printf("%d ", *(p + i));
	}
	
	return 0;
}
7.数组遍历的四种方式:
char ac[] = {0,1,2,3,4,5,6,7,8,9,-1};
int i;
int sz = sizeof(ac)/sizeof(ac[0];

for( i=0;i
	printf("%dn",ac[i]);
}

for(p=ac; *p!=-1;p++){
	printf("%dn",*p);
}

while(*p!=-1){
	printf("%dn",*p++);
}

for(vp = &ac[sz]; vp > &values[0];)
{
  *--vp = 0;
}
六.函数与指针之间的关系 1.函数指针

我们先来看看下面这段代码及它的输出结果:

输出的是两个地址,这两个地址都是 test 函数的地址,也就是说函数名以及&函数名都表示函数的地址。

那我们的函数的地址要想保存起来,怎么保存?

int类型变量的地址用int类型的指针保存,数组地址用数组指针保存,那么函数地址当然是用对应的函数指针保存了。

函数指针的表示与使用:

int(*fucp)(int x,int y) = &Add;//Add是一个函数

fucp先和*结合,说明fucp是指针,指针指向的是一个函数,指向的函数有两个int类型的参数,返回值类型为int。

int (*fucp)(int, int) = Add;//加不加&都可以
int sum = (*pf)(2,3);//调用函数
int sum = pf(2, 3);//不解引用也可以直接通过函数指针调用函数

阅读两段有趣的代码:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

对于代码1,代码可拆分为( * ( void (*)() )0) (),这表示把0强制类型转换为void (*)()类型的函数指针,再解引用0地址获取这个参数为无参,返回类型是void的函数并进行调用。

对于代码2,代码可拆分为void ( * signal(int , void(*)(int) ) )(int),signal是一个函数声明,它有两个参数,第一个参数是int,第二个参数是一个函数指针,该函数指针指向的函数只有一个参数,返回值类型是void。

那么signal这个函数的返回值类型是什么呢?

去掉signal(int , void(*)(int) )后剩下的void( * )(int)就表示signal这个函数的返回值类型,很显然,它的返回值类型是一个函数指针,该函数指针指向的函数的返回值类型是void类型,且只有一个int类型的参数。

代码2太复杂,如何简化?

typedef void(*pfun_t)(int);//这表示void(*)(int)这个函数指针的别名是pfun_t
pfun_t signal(int, pfun_t);
2.函数指针数组

顾名思义,函数指针数组表示这个数组中存储的是函数指针,即函数地址。

那函数指针的数组如何定义呢?

int (*parr[10]])();

parr先和 [] 结合,说明parr是数组,数组的类型是什么呢?去掉parr[10]我们就可以知道, 即该数组是 int (*)() 类型的,存放的是函数指针,也就是函数的地址。

函数指针数组有什么作用呢?

转移表:我们可以通过访问函数指针数组中的函数地址来调用函数

我们以一个简单的计算器作为例子:

#include 

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( "*************************n" );
    printf( "请选择:" );
    scanf( "%d", &input);
    switch (input)
   {
    case 1:
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = add(x, y);
       printf( "ret = %dn", ret);
       break;
    case 2:
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = sub(x, y);
       printf( "ret = %dn", ret);
       break;
    case 3:
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = mul(x, y);
       printf( "ret = %dn", ret);
       break;
    case 4:
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = div(x, y);
       printf( "ret = %dn", ret);
       break;
    case 0:
        printf("退出程序n");
		break;
    default:
       printf( "选择错误n" );
       break;
   }
} while (input);
 
  return 0;
}

那使用函数指针数组是怎样实现的呢?

#include 
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 }; 
  while (input)
  {
     printf( "*************************n" );
     printf( " 1:add      2:sub n" );
     printf( " 3:mul      4:div n" );
     printf( "*************************n" );
     printf( "请选择:" );
   scanf( "%d", &input);
     if ((input <= 4 && input >= 1))
    {
     	printf( "输入操作数:" );
        scanf( "%d %d", &x, &y);
        ret = (*p[input])(x, y);
        //通过访问函数指针数组中的函数地址来调用函数
    }
     else
       printf( "输入有误n" );
       
     printf( "ret = %dn", ret);
  }
   return 0;
}

如上述代码的注释处,通过访问函数指针数组中的函数地址来调用函数,这就是函数指针数组的一个常见使用方式。

3.指向函数指针数组的指针

指向函数指针数组的指针是一个指针,指针指向一个数组 ,数组的元素都是函数指针 。

如何定义?

void (*pArr[5])(const char* str);
//pArr是一个数组,数组元素是一个函数指针,该函数指针指向的函数参数是const char* str,返回值类型是void

void (*(*ppfunArr)[10])(const char*) = &pArr;
//这是一个指向函数指针数组的指针
4.回调函数

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

那回调函数有什么作用呢?

在探究回调函数的作用时,我们先简单了解一下qsort函数,qsort函数的作用是进行一次快速排序,qsort函数的原型如下:

void qsort( void *base, 
			size_t num, 
			size_t width,
			int (__cdecl *compare )(const void *elem1, const void *elem2 ) );

我们可以看到qsort函数有4个参数,而且看起来很复杂,接下来我们就逐个分析qsort函数的各个参数。

qsort函数的第一个参数是void* base,它用于接收待排序元素的起始元素地址,既然是用于接收起始元素的地址,为什么不用char* base或者int* base呢?

这是因为设计者在设计这个函数的时候,不知道待排序元素是什么类型的,如果待排序元素是int类型,那么用户在传参的时候就会传int类型元素的地址,如果待排序元素是float类型,那么用户在传参的时候就会传float类型元素的地址,所以这里设计为void* base,表示不知道传进来的是什么类型元素的地址,统一用void类型的指针变量来接收。

既然提到了void型指针,这里我们顺便了解一下void型指针的一些特点:

  • void* 是一种无类型的指针,无具体类型的指针
  • void* 的指针变量可以存放任意类型的地址
  • void* 的指针不能直接进行解引用操作
  • void* 的指针不能直接进行+—整数

qsort函数的第二个参数是size_t num,这仅仅表示待排序元素的数量,没有什么可说的。

qsort函数的第三个参数是size_t width,这表示待排序元素所占字节大小,那这个参数有什么作用呢?

前面已经提到过因为函数设计者不知道待排序元素的起始元素是什么类型,可能是int,也可能是float或者double,故起始元素的地址统一用void类型的指针变量来接收。

那么问题又来了,我们无法是无法通过void指针来访问传进来的起始元素,也就无法通过void指针访问到后面的待排序元素,那我们该如何利用指针来访问元素呢?

这个时候这个参数的作用就体现出来了,我们可以将void* base,强制类型转化为char* base,这时候通过解引用*base我们就能访问到起始元素的第一个字节,*(base+1)就能访问到起始元素的第二个字节,以此类推*(base+i)访问到起始元素的第i+1个字节,因为width这个变量表示元素所占字节数,base+width-1表示访问该元素的第width个字节,即最后一个字节,所以(base+width)表示跳过第一个元素,指向第二个元素,*(base+width)就表示访问第二个元素,那么以此类推*(base+i * width)表示访问第i+1个元素。

也就是说,将void* base强制类型转化char* base后,通过width,我们可以访问到所有待排序元素。

qsort函数的第4个参数是int (__cdecl *compare )(const void *elem1, const void *elem2 ) ,它是一个函数指针,指向的是一个比较函数,返回值类型为int,参数为const void *elem1和const void *elem2 。

了解了qsort函数之后,我们就通过冒泡排序模拟实现qsort函数来探究一下回调函数的使用:

#include 

//qsort函数的使用者需要根据自己待排序元素的类型来实现一个cmp函数
int int_cmp(const void * p1, const void * p2)
{
	 return (*( int *)p1 - *(int *) p2);
}

//以下为函数设计者的设计思路
//因为我们作为函数设计者无法知道用户待排序元素的类型,所以我们只能通过交换元素底层的字节来实现元素的交换
//指针p1和p2分别保存待交换的两个元素的地址
//函数参数的size表示元素所占字节数

void _swap(void *p1, void * p2, int size)
{
  int i = 0;
  for (i = 0; i< size; i++)
 {
 //因为我们不知道元素是什么类型,无法直接进行交换
 //所以下面开始以字节为单位进行交换,把两个元素中的所有字节交换过来,即实现了元素的交换
  //根据前面的分析我们可以知道*(( char *)p1 + i)表示元素的第i+1个字节
//下面三行代码完成了1个字节的交换,通过for循环完成所有字节的交换,即实现元素的交换
    char tmp = *((char *)p1 + i);
    *(( char *)p1 + i) = *((char *) p2 + i);
    *(( char *)p2 + i) = tmp;
 }
}

//交换函数设计好后,我们开始设计排序函数
//通过冒泡排序模拟实现qsort函数,4个参数前面已经做过详细解释,这里不再赘述
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
  int i = 0;
  int j = 0;
  
  //外层for循环控制轮数
  for (i = 0; i< count - 1; i++)
 {
 //内层for循环控制每一轮比较的次数
  	for (j = 0; j
   	//根据前面的分析(char *) base + j*size,指向第j+1个元素
     	if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
    	 {
      		  _swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
   		  }
   	}
 }
}

int main()
{
 	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	 //char *arr[] = {"aaaa","dddd","cccc","bbbb"};
 	int i = 0;
	bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
 	for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
	{
  		printf( "%d ", arr[i]);
	}
  	printf("n");
  	
  	return 0;
}

看到这里,我们就用回调函数成功实现了qsort函数,我们将cmp函数的地址作为参数传入bubble函数中,然后在bubble函数中通过指针来调用cmp函数,这就是利用了回调函数,可能大家会有个疑问,为什么用利用回调函数的机制,绕这么大的一个圈子来调用cmp函数呢,我们在bubble函数中直接调用不可以吗?

答案是当然不可以,cmp函数是由使用者根据所处情况自己实现的,比如使用者想比较字符串,那么他就设计一个比较字符串的cmp函数,使用者想比较整型元素,那他就实现一个比较整型的cmp函数,cmp是由使用者实现的,我们这个时候都没有设计cmp函数,我们又如何调用cmp函数呢?

所以这个时候回调函数机制的作用就非常明显了,我们将函数指针作为bubble函数的参数,使用者调用bubble函数的时候,需要将自己实现的cmp函数的地址传入bubble函数,然后在bubble函数内部通过指针调用cmp函数。

通过这个例子,我们可以把回调函数可以理解成一种跨时空调用,即回调函数机制实现了前人设计的A函数调用后人实现的B函数。

前人设计A(bubble)函数的时候知道后人一定会用到B(cmp)函数,但是B(cmp)函数只能通过后人根据自己的使用情况来实现,于是前人就在A(bubble)函数中设计一个函数指针的参数用于接收后人自己实现的B(cmp)函数的地址,然后在A(bubble)函数内部通过函数指针调用B(cmp)函数。

这样的一个回调函数机制实现了前人设计的A函数调用后人实现的B函数,因此回调函数可以理解成一种跨时空的调用。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/957992.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号