指针
前言
在这部分,需要大家花时间理解和练习
用什么错误和缺失,也欢迎大家补充!!!
1. 内存和地址
在现实生活中,对于一个任何一个地方都有对应的地址
同样地,在计算机中我们把内存单元的编号也称为地址
在C语言中我们称其为 指针
对于一个内存单元,其占一个字节,一个字节又是八个比特位
1 byte = 8 bit
所以对于一个“门牌号”来说,就是一个字节
2. 指针变量
2.1 &取地址操作符
我们有了解到,C语言创建变量实际上是在内存中申请空间
比如:
int a = 0;
我们定义了一个变量a,即在内存中申请了一片空间
那么我们怎么得到它的地址呢?
通过&取地址操作符就可以得到
2.2 指针变量
我们可以通过指针变量,将地址存放在一个变量之中
int a = 0;
int *pa = &a;
定义了一个指针变量 pa,pa的值是a的地址
值得注意的是,对于指针变量来说任何值都会被认为是地址
例如:
int *pa = 100; // 会把pa指向的地址为100(需要转化为16进制)
2.3 * 解引用操作符
通过一个变量的地址,我们可以访问到这个变量
需要用到 “ * ”解引用操作符
例如:
int main()
{
int a = 20;
// &a -- '&'取地址操作符,拿到变量a的地址
printf("%p\n",&a);
int* pa = &a;
*pa = 200;
//* -- 解引用操作符(间接访问操作符)
// 这里“*pa”指向变量“a”:理解为:“ a ” 是背后大哥不方便干些事拜托 “ pa ”(指针变量)干事
printf("%d\n", a);
return 0;
}
来看运行结果:
a的值被改变为了200,说明我们通过指针变量,修改了指针指向的变量
这里我们也可以说明:
取地址 &
解引用 * 可以说是互为逆运算
2.4 指针变量的大小
指针变量的大小在x64、x86环境下有差异
- 在x64环境下,一个指针变量的大小为4字节
- 在x86环境下,一个指针变量的大小为8字节
• 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
3. 指针类型的意义
这个是非常重要的
3.1 访问权限
不同指针类型所访问的内存字节长是不一样的
例如:
int main()
{
int a = 0x11223344;
//一个十六进制数占2进制4个为;2个占一个字节(八个比特位)
int* pa = &a; //一次能访问四个字节
*pa = 0;
return 0;
}
int main()
{
int a = 0x11223344;
char* pa = (char*)&a; //强转一下不会报警告
*pa = 0;
return 0;
}
对比上面两段代码,它们对于同一类型的变量,放在了不同类型的指针变量之中
结果会用什么差异呢?
(后期会解决数据存放的顺序问题,高位在高地址还是低位在高地址)
我们发现:
- 不同类型的指针变量访问的内存单元个数不一样
- 指针变量从低地址开始访问的
结论:
指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作几个字节)。
比如: char* 的指针解引⽤就只能访问⼀个字节,而 int* 的指针的解引用就能访问四个字节
3.2 指针“步调”
对于一个指针变量来说,他是指向内存中的某个位置的,那么对于整数的加减法来说,其是有意义的…
即: 指针 ± 整数
而我把指针“跨越”的字节数称为: 指针的步调
举例子:
#include <stdio.h>
int main()
{
int n = 0x11223344;
char* pc = (char*)&n;
int* pi = &n;
printf("&n = %p\n", &n);
printf("&n+1 = %p\n", &n + 1); // 自己类型取地址+1
printf("pi+1 = %p\n", pi + 1);// int* 类型 +1
printf("pc+1 = %p\n", pc + 1); // char* 类型
return 0;
}
根据运行结果我们作图:
实际上:
- int *pa:
pa + 1 ----> +1 * sizeof(int)
pa + n ----> +n * sizeof(int) - char *pa:
pa + 1 ----> +1 * sizeof(char)
pa + n ----> +n * sizeof(char)
我们发现:
指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)
3.3 void* 类型的指针
在指针类型中有⼀种特殊的类型是 void* 类型的
可以理解为无具体类型的指针(或者叫泛型指针)。
这种类型的指针可以⽤来接受任意类型地址。
但是也有局限性:
void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算。
例如:
//使⽤void* 类型的指针接收地址:
int main()
{
int a = 10;
void* pa = &a;
void* pc = &a;
//*pa = 10; //无法修改
//*pc = 0; //无法修改
return 0;
}
⼀般 void* 类型的指针是使⽤在函数参数的部分
⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果
4. const修饰
常属性
const修饰这个变量的时候,叫:常变量
这个被修饰的量 “本质” 上还是变量只是不能被修改!
( 所以还是不能用于VS中的变长数组 )
例如:
int main()
{
const int num = 100;
num = 200;
printf("%d\n", num);
return 0;
}
报错无法修改
但是我们通过指针变量可以修改:
int main()
{
const int n = 10;
//n = 20;(不能使用)
int* pc = &n; // 相当于翻窗户的形式,但不希望被修改
*pc = 200;
printf("%d\n", n); // 200
return 0;
}
但是我们用const 修饰了n就是希望其不被修改,但是我们仍然修改了n
这时候我们就可以用const来修饰指针变量了
const修饰指针变量
(可以放在*的左边也可以右边,但是效果不同)
首先需要了解关于指针p的三个量:
- p,p里边放着一个地址;
- *p,p指向的那个对象;
- &p,表示指针变量p的地址;
代码一:
int main()
{
int n = 10;
int m = 100;
const int* p = &n;
//放在“ * ”左边:限制的是“ *p ”(指针指向的内容),但是指针变量本身可以改变!
//*p = 20; //error!
p = &m; //ok
return 0;
}
代码二:
int main()
{
int n = 10;
int m = 100;
int* const p = &n;
//放在“ * ”右边:限制的是“ p ”(指针变量本身),
//但是可以通过指针变量修改“ *p ”指向的内容!!
*p = 20; //ok
//p = &m; //error
return 0;
}
通过上面两个案例我们发现:
const可以修饰:
- const int* pa
- int * const pa
- const int * const pa(可以两个都修饰)
5. 指针运算
5.1 指针 ± 整数
上面已经讲过了作用
来看一个实例(用指针访问数组元素):
// 数组在内存中是连续存放的!用指针加减(+1 四个字节步长)
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
//需要一个整型一个整型地走,必须要定为int*类型(一方面是步调,一方面是权限)!
//char* p; 在 0x11223344中 只会访问 44 而忽略掉剩下的数
// 后面会谈到“数组名”
for (int i = 0; i < k; i++)
{
printf("%d ", *p);
//printf("%d ",*(p + i));
p++;
}
return 0;
}
我们也可以看到: p[i] == *(p + i) 是完全等价的!!
5.2 指针 - 指针
就像时间一样:
时间 - 时间是有意义的, 但是时间 + 时间是没有意义的
指针也是一样:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//数组下标大的地址高,下标小的地址小!
printf("%d ", &arr[9] - &arr[0]); // 9
printf("%d ", &arr[0] - &arr[9]); // -9
return;
}
6. 野指针
我们都知道指针是指向一块内存单元
但是如果这个内存单元我们没有使用权呢?
概念:
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1 成因分析
- 指针未初始化
#include <stdio.h>
int main()
{
int* p; //局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
- 指针指向越界访问
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i <= 11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
- 指针指向的区域被释放
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test()
printf("%d\n", *p);
return 0;
}
平时注意这三个方面就可以尽量避免野指针的问题
6.2 NULL
在C语言中这是空指针的用法:
即,我们可以在一个指针变量不被使用的时候即使把这个指针变量置为空,方便可管理
7. assert断言
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”
简单来说就是:
assert() 中 ()里面是一个判断内容:
- 如果为真就不报错
- 如果为假就报错(逻辑)
对于空指针来说:
NULL == 0 ,在逻辑上为假
#include <assert.h>
int main()
{
int a = 10;
int* p = NULL;
assert(p != NULL);//后面条件为假报错;后面条件为 真 不报错
return 0;
}
assert() 的使⽤对程序员是非常友好的,使⽤ assert() 有几个好处:
- 它不仅能自动标识文件和出问题的行号,
- 还有⼀种⽆无需更改代码就能开启或关闭 assert() 的机制。
如果已经确认程序没有问题,不需要再做断⾔,
就在 #include <assert.h> 语句的前面,定义⼀个宏 NDEBUG。
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁用文件中所有的 assert() 语句。 - 如果程序⼜出现问题,可以移除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。
assert() 的缺点是:
- 因为引入了额外的检查,增加了程序的运行时间。
⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁用 assert 就⾏,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,
在 Release 版本不影响用户使用时程序的效率
8 传值和传值调用
在之前函数部分,我们已经了解了函数的形参是实参的一份临时拷贝,所以改变形参是无法改变实参的。所以,我们无法在函数内部改变实参的。
但是如果我们拿到地址就不同了:
代码一:
//写一个函数交换两个整型变量的值
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;
}
代码二:
#include <stdio.h>
void Swap2(int* px, int* py)
{
int tmp = 0;
tmp = *px; //z = a;
*px = *py; //a = b;
*py = tmp; //b = z;
}
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;
}
对比上面代码,代码二能实现a,b的交换,因为它采用的是传址调用。
虽然,px,py都只是临时变量,但是他却存放的是a,b变量的地址,
所以可以通过地址解引用来改变a,b变量的值
9 指针类型
9.1 指针与一维数组
9.1.1 数组首元素
实际上:
数组名就是,数组首元素的地址。
一个数组名为arr的数组,
实际上: arr == &arr[0]
测试一下:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int* pc = arr; // !! 数组名的地址就是数组首元素的地址!!
printf("p = %p\n", p); //这里的p == pc;
printf("pc = %p", pc);
return 0;
}
没错二者地址是一样的,而且二者的类型是一样的
所以:
代码一:
int main()
{
int arr[4] = { 0 };
for (int i = 1; i <= 4; i++)
{
scanf("%d", arr + i - 1);
}
return 0;
}
代码二:
int main()
{
int arr[4] = { 0 };
for (int i = 1; i <= 4; i++)
{
scanf("%d", &arr[i-1]);
}
return 0;
}
二者是完全等价的!
对于数组名是数组首元素地址有两个例外:
- sizeof(数值名) 这里的数组名表示整个数组,计算的是“ 整个数组 ”的大小,单位是字节!
- &数组名,这里的数组名也表示整个数组,取出的是整个数组的地址!
(两个都是有且仅是单独的数组名)
对于2举个例子:
int arr[10] = {0}; //定义了一个数组
// 我们之前知道arr类型为 int[10],所以我们对其取地址
// &arr -- 得到的类型就应该是 int(*)[10](后面会介绍这个类型),
//所以说是取出的整个数组
再对比:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0]+1); //走了4个字节
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr+1); //走了4个字节
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr+1); //走了40个字节!!!
return 0;
}
会发现,它们指向的起始地址都是相同的。
但是我们之前也谈到了,指针类型决定了访问权限和步调
再来举个例子:
// 指针访问数组:
int main()
{
int arr[10] = { 0 };
//输⼊
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//输⼊
int* p = arr;
//int*p = &arr[0];//也可以这样写
for (i = 0; i < sz; i++)
{
//scanf("%d", p + i);
scanf("%d", &p[i]);
//scanf("%d", arr+i);//也可以这样写
//scanf("%d", &arr[i]);//也可以这样写
}
//输出
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
//!!注意不能写成 *p + i ---> 表示 *p 代表的元素 + 1;
//printf("%d ", p[i]);
//printf("%d ", *(arr + i));
//printf("%d ", i[arr]);
}
return 0;
}
所以我们也可以看到的是: p[i] == *(p + i) == arr[i] == *(arr + i);
小结一下:
数组名是数组的首元素:
两个例外:
1.sizeof(数值名)
2.&数组名
- 数组就是数组,是一块连续的空间,是可以存放一个或多个数组的
- 指针变量就是一个变量,是可以存放地址的变量
指针和数组不是一回事
但是可以是一种指针来访问数组
为什么可以使用指针来访问数组呢?
1.数组在内存中是连续存放的
2.指针的元素是很方便可以遍历数组,取出数组的内容(指针运算)
所以来看:
一维数组传参的本质是什么呢?
其实就是传入的是数组首元素的地址:
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);//10
test(arr); // 与所处环境有关
return 0;
}
数组传参本质,传的是数组首元素的地址
所以 形参即使写成数组的形式,本质上也是一个指针变量
但是数组传参的时候,形参可以写成数组,也可以写成指针
所以即使写成数组,本质上还是指针;
所以想在函数里打印整型等数组内容,可以传大小:
void test(int* arr,int x)
{
int i = 0;
for (i = 0; i < x; i++)
{
printf("%d ", arr[i]);
//printf("%d ", *(arr + i)); //打印里面的内容一定要用“ * ”;
}
}
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,sz1);
return 0;
}
9.1.2 指针数组
听名字就是一个数组,但是里面的变量都是指针类型的
例一:
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* arr[3] = {&a,&b,&c};
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%p\n",arr[i]);
//printf("%d\n", *(arr[i]));
//printf("%d\n", *(*(arr + i)));
}
return 0;
}
接下来,我们用指针数组模拟二维数组:
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[3] = { arr1, arr2, arr3 }; //parr中存放的就是地址
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这个数组我们通过下标访问到元素 arr1,arr2,arr3等都是一个数组的首元素地址,所以再通过这些首元素地址来访问其中的元素
即:利用了指针来访问数组元素,可以结合我们之前的案例来分析
9.2 指针与二维数组
9.2.1 数组指针
这个需要与前面的指针数组需要进行区分:
- 指针数组:数组元素是指针变量
- 数组指针:指针指向的是数组
来看下面的案例进行区分:
// !!区别指针数组
int* p1[10];
int (*p2)[10];
//思考⼀下:p1, p2分别是什么?
// p1指针数组
// p2数组指针
//数组指针变量
int (*p)[10];
//解释:
//p先和* 结合,说明p是⼀个指针变量,然后指针指向的是⼀个⼤⼩为10个整型的数组。
//所以 p 是⼀个指针,指向⼀个数组,叫 “数组指针”。
这⾥要注意:
[]的优先级要⾼于* 号的,所以必须加上()来保证p先和* 结合。
我们在前面说了,对一个数组名取地址得到的是整个数组的地址
于是:
int main()
{
int arr[10] = { 1,2,3,4,5 };
int (*p)[10] = &arr; // ---> 数组指针,p中存放的是数组的地址
for (int i = 0; i < 10; i++)
{
printf("%d ", (*p)[i]);//*(*p+i)
// 需要用括号保证结合性
}
return 0;
}
1. arr -- int* arr+1 跳过4个字节
2. &arr[0] -- int* &arr[0]+1 跳过4个字节
3. &arr -- int(*)[10] &arr+1 跳过40个字节
p 是整个数组的地址,解引用后得到的是“首元素的地址”,再通过首元素来访问各个元素
int (*p)[10] = &arr; // 不同于二维数组的“ [] ”
// | | |
// | | |
// | | p指向数组的元素个数,千万不能省略
// | p是数组指针变量名
// p指向的数组的元素类型
// p里面存的就是数组的整个地址;---> p == &arr ---> *p == *&arr == arr;
当然这个数组元素类型可以是其它类型:
例如:char*
int main()
{
char* ch[5]; //指针数组 里面存的是地址
char* (*pc)[5] = &ch; //数组指针
return 0;
}
等等,都是可以的
9.2.2 二维数组传参本质
对于二维数组传参我们是这样写的
void test(int a[3][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 ", *(*(a + i) + j)/*a[i][j]*/);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,3,5,7,9}, {2,4,6,8,10},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
实际上
我们之前讲到过:二维数组是一维数组的数组。
同样地,我们二维数组数组名,也就是首元素的地址,首元素就是第零行的数组,
即整个第零行的一维数组 arr == &arr[0]
所以我们传入的参数 实际上是 —> int (*arr)[5] (数字不能省略)
但是实际上我们传参的时候,不需要刻意写成这样的形式,需要的是:
理解本质,它的底层逻辑是什么,这是需要我们知道的
9.3 二级指针
一般的变量都有地址,那么我们的指针变量肯定是有自己的地址:
int main()
{
int a = 10;
int* pa = &a; //pa 就是一级指针变量
int** ppa = &pa; //ppa就是二级指针变量
printf("%d\n", **ppa); //-->*ppa代表pa ---> *pa 代表a;
return 0;
}
就是如下的关系:
9.4 字符指针变量
字符指针变量有几种形式:
例如:
char c = 'a';
char carr[10] = "abcdef";
char* pc = &c;
char* pcarr = carr;
// 跟整型指针和数组是一样的
还有一种形式:
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit."; // 直接赋值给char* str3
const char* str4 = "hello bit.";
if (str1 == str2) //比较首地址
printf("str1 and str2 are same\n");//1
else
printf("str1 and str2 are not same\n");//2
if (str3 == str4)
printf("str3 and str4 are same\n");//3
else
printf("str3 and str4 are not same\n");//4
return 0;
}
类似于上面str3,str4的赋值,是“常量”字符串赋值。
C/C++会把常量字符串存储到单独的⼀个内存区域,
当几个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。
但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。
所以str1和str2不同,str3和str4相同。
而且我们记得:
常量是不允许被修改的,所以我们只能通过指针去访问这个字符串,不能修改
9.5 函数指针
9.5.1 函数指针变量
就是指向函数的指针
来看语法:
int add(int x, int y)
{
return x + y;
}
int main()
{
//&函数名和函数名都是函数的地址,没有区别;
printf("%p\n", &add);
printf("%p\n", add); //结果一模一样
int (*pf)(int, int) = &add; //pf函数指针变量
//int (*pf)(int x, int y) = &add; //pf函数指针变量
//int (*pf)(int, int) = add; //pf函数指针变量
// int (*)(int,int) ---> 函数指针变量类型
// 调用:
int ret = (*pf)(4,5);
printf("%d\n", ret);
int ret1 = add(4, 5);
printf("%d\n", ret1);
// |
//add就是函数的地址,所以可以不用对pf解引用:如
int ret2 = pf(4, 5);
printf("%d\n", ret2);
return 0;
}
上面的案例讲解了如何定义函数指针变量,和如何使用
9.5.2 函数指针数组
就是由函数指针构成的数组
例如:
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 };
// 0 1 2 3 4
// .....
return 0;
}
上面的int(*p[5])(int x, int y)就是一个函数指针数组,通过这个数组元素可以访问函数
10 typedef关键字
typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。
对已有的类型重命名:
例如:
例一:
typedef unsigned int unit;
//将unsigned int 类型重命名为 unit
int main()
{
unsigned int num;
unit num2;
return 0;
}
例二:
//⽐如我们有数组指针类型 int(*)[5], 需要重命名为 parr_t ,那可以这样写:
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
//函数指针类型的重命名也是⼀样的,
//⽐如,将 void(*)(int) 类型重命名为 pfun_t, 就可以这样写:
typedef void(*pfun_t)(int); //新的类型名必须在*的右边
现在来看下面这段代码:
void (*signal(int, void(*)(int)))(int);
这是个啥呢?
通过一步一步拆解的话,你会发现,这是一个函数声明
来看:
int main()
{
void (*signal(int , void(*)(int)))(int);
//函数声明,不是函数定义
// signal --> 函数名
// (int, void(*)(int)) ---> 函数的参数; int --> 第一个参数,整型 ; void(*)(int) ---> 第二个参数,函数指针类型
// void(*)(int) ---> 返回类型!为函数指针类型
// void(*)(int) signal(int, void(*)(int)) ---> 实际理解但是 语法不能这么写
return 0;
}
// 上面的这段代码是一串函数声明
// 函数名叫:signal
// signal函数的参数有2个
// 第一个参数类型为int
// 第二个参数类型为一种函数指针 void(*)(int)
// signal函数的返回类型也是一个函数指针,类型为 void(*)(int)
我们可以同个typedef进行简化:
例如:
//例子:
typedef void (*pf_t)(int); //---> 把void (*pf_t)(int)类型叫做pf_t
//代码二:
void (*signal(int, void(*)(int)))(int);
//简化:
pf_t signal(int, pf_t);
这样是不是看起来顺眼多了!(typedef这个关键字后期还会用得特别多)
完
感谢观看