【C语言】——指针四:字符指针与函数指针变量

慈云数据 2024-04-23 技术支持 105 0

C语言】——指针四:字符指针与函数指针变量

    • 一、字符指针
    • 二、函数指针变量
      • 2.1、 函数指针变量的创建
      • 2.2、两段有趣的代码
      • 三、typedef关键字
        • 3.1、typedef的使用
        • 3.2、typedef与define比较
        • 四、函数指针数组

          一、字符指针

            在前面的学习中,我们知道有一种指针类型为字符指针: c h a r ∗ char* char∗。下面我们来介绍它的使用方法

            

          使用方法:

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

            

            如果我们想存储字符串,可以用什么方法呢?之前我们一般都是用字符数组,那还有什么办法呢?其实,字符指针也是可以的。

            

          使用方法:

          #include
          int main()
          {
          	const char* pstr = "Hello World";
          	printf("%s\n", pstr);
          	return 0;
          }
          

            在const char* pstr = "hello world";代码中,可能很多小伙伴以为是把整个字符串"hello world"放进字符指针 p s t r pstr pstr 中,但其实,这里本质是把 " h e l l o "hello "hello w o r l d " world" world" 的首字符 ‘ h ’ ‘h’ ‘h’ 的地址放在指针变量 p s t r pstr pstr 中。

            

            至于代码printf("%s\n", pstr);指针 p s t r pstr pstr 并不需要解引用,因为 p r i n t f printf printf 函数打印字符串本质是接收该字符串首元素地址,从该地址开始往后打印,直到遇到 ‘ \0 ’ 停止,解引用反而是错的。

            

            这里,也要随便提一下,代码printf("hello world"),同样不是把整个字符串 " h e l l o "hello "hello  w o r l d " world" world" 传给 p r i n t f printf printf 函数,其本质也是将首字符 ‘ h ’ ‘h’ ‘h’ 的地址传给 p r i n t f printf printf 。  p r i n t f printf printf 再从给来的地址开始打印,直到遇到 ‘ \0 ’ 停下。

            

          在这里插入图片描述

            

            那字符指针与数组指针有什么区别呢?他们最大的区别就是

          • 字符数组里的内容可以修改。
          • 字符指针中放的是常量字符串,内容不可修改。

              

            因此,我们可以在字符指针 c h a r char char*前加上 c o n s t const const 修饰,以确保他不能被修改。

                

              

            下面,我们来看一道题,进一步感受字符指针与字符数组的区别

            int main()
            {
            	char str1[] = "hello world";
            	char str2[] = "hello world";
            	const char* str3 = "hello world";
            	const char* str4 = "hello world";
            	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;
            }
            

            输出结果

            在这里插入图片描述

              

            为什么会这样呢?

              

              这里,其实 s t r 3 str3 str3 和 s t r 4 str4 str4 指向同一个常量字符串,C/C++会把常量字符串存储到单独的一个内存空间(代码段)。

              

              因为常量字符串无法被修改,没必要存储两份,当多个字符指针指向同一个常量字符串时,他们实际会指向同一块内存。

              

              但是用相同的常量字符串去初始化数组就会开辟出不同的内存块。

              

              所以 s t r 1 str1 str1 和 s t r 2 str2 str2 不同, s t r 3 str3 str3 和 s t r 4 str4 str4 相同。

              

              

            二、函数指针变量

            2.1、 函数指针变量的创建

              

              什么是函数指针变量呢?

              

              在前面的学习中(【C语言】—— 指针三 : 参透数组传参的本质)我们了解到数组指针变量,他是用来存放数组指针地址的。同理函数指针变量应该是存放函数地址的,未来能通过他来调用函数。

              

              那么问题来了,函数是否有地址呢?

              

            我们来做个测试:

            #include
            void test()
            {
            	printf("hello world\n");
            }
            int main()
            {
            	printf("test:   %p\n", test);
            	printf("&test:  %p\n", &test);
            	return 0;
            }
            

              

            在这里插入图片描述

              

              可以看到,我们确实打印出了函数的地址,可见,函数是有地址的。

              

              这里与数组有点相似,不论是直接打印数组名还是 &数组名,都能打印出地址,不同的是数组的数组名表示的是数组首元素的地址,而 &数组名 表示的是整个数组的地址,他们仅仅只是在数值上相等,类型是不一样的。而对于函数来说函数名和 &函数名 的效果是一模一样的。

              

              现在我们把函数的地址取出来了,那该存放在哪呢?老办法,将地址放在指针变量。对于函数的地址,当然是放在函数指针变量中啦,而函数指针变量的写法其实和数组指针变量非常相似(详情请看【C语言】—— 指针三 : 参透数组传参的本质)。

              

            如下:

            void test()
            {
            	printf("hello world\n");
            }
            void (*pf1)() = &test;
            void (pf2)() = test;
            int Add(int x, int y)
            {
            	return x + y;
            }
            int (*pf3)(int x, int y) = Add;
            int (*pf4)(int, int) = &Add;//x 和 y 写上或者省略都是可以的
            

              

              注:函数指针变量中,参数类型的名字可省略,对于函数指针变量来说,重要的是参数类型和返回类型,参数名叫什么并不重要。

              

              

            函数指针类型解析:

            图

              

              学习函数指针后,我们就可以通过函数指针来调用指针指向的函数啦

            #include
            int Add(int x, int y)
            {
            	return x + y;
            }
            int main()
            {
            	int (*pf)(int, int) = Add;
            	printf("%d\n", Add(1, 2));
            	printf("%d\n", (*pf)(2, 3));
            	printf("%d\n", pf(3, 4));
            	
            	return 0;
            }
            

            运行结果:

            在这里插入图片描述

              

              在这里 Add(1, 2);是通过函数名调用;而(*pf)(2, 3);和pf(3, 4);都是通过函数指针调用。

              

              两种函数指针的调用效果是一样的,因此对函数指针来说,解引用可以看作是摆设。

              

              

            2.2、两段有趣的代码

              

              接下来,我们来看两段有趣的代码:

              

            ( *( void( * )( ) ) 0)( );
            

            我们来慢慢分析

              

            • 先来看 void( * )( )部分,是不是觉得很熟呢?,如果你还看不出来,那这样呢: void( *p )( );现在认出来了吧。没错它是一个指针变量,一个变量去掉变量名是什么?是类型。没错 void( * )( )是一个函数指针类型。

                

            • 那一个类型加上小括号是什么?是强制类型转换!所以(void(*)())0即是把 0 强制类型转换成函数指针类型(原来是 i n t int int 类型),如果还不理解,我们可以这样来看(void(*)())0x0012ff40。这样是不是清晰了许多呢?

                

            • 既然将 0 强转成函数指针类型,那即意味着 0 地址处放着一个函数,该函数返回类型是 v o i d void void,没有参数。

                

            • 接着,对位于该地址的函数进行解引用调用该函数:( * ( void( * )( ) ) 0)( ),

                

            • 因为这个函数没有参数(由类型void(*)()可知),所以后面的小括号(参数列表)不填。

                

            • 整句代码的意思是:调用在地址 0 出的函数,该函数的返回类型是 v o i d void void ,没有参数。

                

              void (*signal(int, void(*)(int)))(int);
              

              这句代码也让我们一起来分析分析

                

              • 先来看里面的部分signal(int,void(*)(int))很明显,signal是函数名,而int和void(*)(int)是函数的参数类型。

                  

              • 那前面的*是什么意思?解引用吗?其实我们不妨顺着函数的思路往下想,一个函数有了函数名和参数类型,还差什么?是返回类型。那这个函数的返回类型是什么?剩下的部分就是返回类型。

                  

              • 其实,该函数的返回类型被劈开了,我们将它的函数名和参数类型拿走,剩下的就是它的返回类型void(*  )(int),如果还不清晰,我们可以写成这样来理解(接下来的写法是错误的,仅仅是为了方便理解,题目写法是正确的)void(*)()  signal  (int,void(*)(int))。

                  

              • 所以,这一句代码是一个函数声明,函数名是signal;返回类型是void(*)();参数类型是int和void(*)()。

                注:以上两段代码均出自 《C陷阱和缺陷》。

                  

                  

                三、typedef关键字

                3.1、typedef的使用

                  

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

                  

                  比如,如果觉得unsigned int写起来不方便,我们可以写成uint就方便多了,那我们可以这样写

                typedef unsigned int uint;
                //将unsigned int类型重命名为uint
                

                  

                  我们之前简单提到的结构体类型(详情请看【C语言】——详解操作符(下)),觉得每次都要加 s t r u c t struct struct 太麻烦了,那我们可以通过 t y p e d e f typedef typedef 将其重命名。

                typedef struct student
                {
                	char name[20];
                	int age;
                }student;
                //将结构体类型struct student重命名为student
                

                  

                  那如果是指针类型,可不可以通过 t y p e d e f typedef typedef 来重命名呢?答案是肯定的。比如,将int*重命名成ptr_t,我们可以这样写:

                typedef int* ptr_t;
                

                  

                  但对于函数指针和数组指针稍微有点区别。区别在哪呢?新的类型名的位置不同。

                  比如我们将数组指针类型int(*)[10]重命名为parr_t,我们可以这么写:

                typedef int(*parr_t)[5];
                

                  

                  同样,函数指针变量的重命名也是一样的,比如将void(*)(int)重命名为pf_t,可以这样写:

                typedef char(*pf_t)(int, int);
                

                  

                那么现在,我们就可以用 t y p e d e f typedef typedef 将代码void (*signal(int, void(*)(int)))(int);简化

                typedef void(*pfun_t)(int);
                pfun_t singal(int, pfun_t);
                

                  

                3.2、typedef与define比较

                  

                  想了解 t y p e d e f typedef typedef 与 d e f i n e define define 的区别,我们先来一组比较:

                typedef int* ptr_t;
                #define PTR_T int*
                ptr_t p1, p2;
                PTR_T p3, p4;
                

                  

                他们有什么区别呢?

                • p1和p2都是指针变量。
                • 而p3是指针变量,p4是整形变量。

                  为什么会这样呢?

                    

                    对于ptr_t,他是通过 t y p e d e f typedef typedef 来修饰的, t y p e d e f typedef typedef 的作用就是重命名,因此pyr_t就是int*,他们是画等号的。

                    

                    而对于PTR_T,他是通过 d e f i n e define define 修饰的,PTR_T仅仅是替换int*。int* p3、p4;中, *给了 p3, p3 是 指针变量,而p4只剩int了,是整形变量。

                    

                    

                  四、函数指针数组

                    

                    数组是一个存放相同类型数据存储空间,之前,我们已经学过了指针数组(详情请看【C语言】—— 指针二 : 初识指针(下))

                  int* arr[10];
                  //数组的每个元素是int*
                  

                    

                    那要把一个函数的地址放在数组中,这个数组就叫函数指针数组,那函数指针数组该怎么定义呢?

                  int(*parr1[3])();
                  int* parr2[3]();
                  int(*)()parr3[3];
                  

                  答案是: p a r r 1 parr1 parr1

                    

                    parr1先和[]结合,表示一个parr1是一个数组,那数组中的元素类型是什么呢?是int(*)()类型的函数指针。

                    

                    那么函数指针数组有什么用呢?别急,敬请收看下一章:【C语言】——指针五:转移表与回调函数。

                    

                    

                    

                    


                    好啦,本期关于字符指针和函数指针就介绍到这里啦,希望本期博客能对你有所帮助,同时,如果有错误的地方请多多指正,让我们在C语言的学习路上一起进步!

微信扫一扫加客服

微信扫一扫加客服

点击启动AI问答
Draggable Icon