【C语言】——指针四:字符指针与函数指针变量
- 一、字符指针
- 二、函数指针变量
- 2.1、 函数指针变量的创建
- 2.2、两段有趣的代码
- 三、typedef关键字
- 3.1、typedef的使用
- 3.2、typedef与define比较
- 四、函数指针数组
一、字符指针
在前面的学习中,我们知道有一种指针类型为字符指针:
c
h
a
r
∗
char*
char∗。下面我们来介绍它的使用方法。
使用方法:
#include<stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'w';
return 0;
}
如果我们想存储字符串
,可以用什么方法呢?之前我们一般都是用字符数组
,那还有什么办法呢?其实,字符指针
也是可以的。
使用方法:
#include<stdio.h>
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<stdio.h>
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<stdio.h>
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语言的学习路上一起进步!