C语言指针超详解——强化篇

C语言指针系列文章目录

入门篇
强化篇

文章目录

  • C语言指针系列文章目录
  • 1. assert 断言
  • 2. 指针的使用和传址调用
    • 2. 1 strlen的模拟实现
    • 2. 2 传值调用和传址调用
  • 3. 数组名的理解
  • 4. 使用指针访问数组
  • 5. 一维数组传参的本质
  • 6. 冒泡排序
  • 7. 二级指针
  • 8. 指针数组
  • 9. 指针数组模拟实现二维数组


1. assert 断言

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

assert(p != NULL);

上面代码在程序运行到这一行语句时,验证变量 p是否等于 NULL。如果确实不等于 NULL,程序继续运行,否则就会终止运行,并且给出报错信息提示。
assert()宏接受一个表达式作为参数。如果该表达式为真(返回值非零),assert()不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零),assert()就会报错,在标准错误流 stderr 中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号
断言失败

assert()的使用对程序员是非常友好的,使用 assert()有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭 assert()的机制。如果已经确认程序没有问题,不需要再做断言,就在 #include<assert.h>语句的前面,定义一个宏 NDEBUG

#define NDEBUG
#include<stdio.h>

NEDBUG
然后,重新编译程序,编译器就会禁用文件中所有的 assert()语句。如果程序又出现问题,可以移除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启用了 assert()语句。

assert()的缺点是,因为引入了额外的检查,增加了程序的运行时间。

一般我们可以在 Debug 中使用,在 Release 版本中选择禁用 assert 就行,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响用户使用时程序的效率。
release

2. 指针的使用和传址调用

2. 1 strlen的模拟实现

库函数 strlen(包含在<string.h>中)的功能是求字符串长度,统计的是字符串中 ‘\0’ 之前的字符的个数。
函数原型如下:

size_t strlen ( const char * str );

不妨来尝试写一下:
方法一,计数器:参数str接收一个字符串的起始地址,然后开始统计字符串中\0之前的字符个数,最终返回长度。
如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是\0 字符,计数器就+1,这样直到\0 就停止。

方法二,指针-指针:参数str接收一个字符串的起始地址后,将其放入 str2中,在通过 str2 进行遍历字符串找到 ‘\0’ ,返回两个指针的差。

代码示例:

#include<stdio.h>
#include<string.h>//方法一:计数器
size_t my_strlen(const char* str)
{int count = 0;while (*str)//'\0'的码值为0,当*str=='\0'时,循环停止{count++;str++;}return count;
}
//方法二:指针-指针
size_t my_strlen(const char* str)
{const char* str2 = str;while (*str2)str2++;return str2 - str;
}int main()
{char a[] = "abcdef";printf("%zd ", strlen(a));printf("%zd ", my_strlen(a));return 0;
}

2. 2 传值调用和传址调用

学习指针的目的是使用指针解决问题,那什么问题,非指针不可呢?

来思考这样的问题:
设计一个函数,交换两个变量的值。
不加过多思考,可能会写出这样的代码:

void Swap(int a, int b)
{int c = a;a = b;b = c;
}

当然,只要仔细思考一下,函数传参时形参是实参的拷贝,就能明白这个函数实际上没有任何意义了。
详情可见:C语言函数第4节
实际上,这种调用方式是把变量本身的值传递给了函数,这种方法就叫做传值调用

那怎么实现这个函数呢?

我们现在要解决的就是当调用Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换了
那么就可以使用指针了,在main函数中将a和b的地址传递给Swap函数,Swap函数里边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。

我们对Swap 函数进行改进,并进行测试:

#include<stdio.h>void Swap(int* a, int* b)
{int c = *a;*a = *b;*b = c;
}int main()
{int a = 9;int b = 5;printf("a=%d b=%d\n", a, b);Swap(&a, &b);printf("a=%d b=%d\n", a, b);return 0;
}

测试结果
我们可以看到再次实现的Swap,顺利完成了任务,这里调用Swap函数的时候是将变量的地址传递给了函数,这种函数调用方式叫:传址调用
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所
以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改
主调函数中的变量的值,就需要传址调用

3. 数组名的理解

或许你见过或是写过这样的代码:

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

这里使用 &arr[0] 的方式拿到了数组第一个元素的地址。
但是其实数组名本来就是地址,而且是数组首元素的地址,我们来做个测试。

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

p 中存放的是 &a,而 p2 中存放的是 a,将它们用 %p 占位符打印出来:
打印结果
很显然,这两个指针指向的地址是完全相同的!
也就是说:数组名就是数组首元素(第一个元素)的地址

当然没看到这里,你可能会有一些疑惑,如果说数组名是数组首元素的地址,那下面的代码怎么理解呢?

#include<stdio.h>
int main()
{int a[] = { 0,1,2,3,4,5,6,7,8,9 };size_t sz = sizeof(a);printf("%zd", sz);//输出结果为 40return 0;
}

输出的结果是:40,如果arr是数组首元素的地址,那输出应该的应该是 4 / 8 (在x86和x64环境下,地址的大小不相同)才对。
其实数组名就是数组首元素(第一个元素)的地址是对的,但是有两个例外:

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

除此之外,任何地方使用数组名,数组名都表示首元素的地址。

来试试这个代码:

#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);printf("&arr    = %p\n", &arr);return 0;
}

很明显会发现,这三个打印的结果是完全相同的,那 arr 和 &arr 取出的地址有什么区别呢?

#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[0]+1 = %p\n", &arr[0] + 1);printf("arr       = %p\n", arr);printf("arr+1     = %p\n", arr + 1);printf("&arr      = %p\n", &arr);printf("&arr+1    = %p\n", &arr + 1);return 0;
}

输出结果
这里我们发现 &arr[0] 和 &arr[0]+1 相差4个字节,arr和arr+1相差4个字节,是因为 &arr[0] 和 arr 都是首元素的地址,+1 就是跳过一个元素
但是 &arr 和 &arr+1相差40个字节,这就是因为 &arr 是数组的地址,+1操作是跳过整个数组的。(关于这里,会在下一篇博客中进行详解)

总结:数组名是数组首元素的地址,但是有2个例外

4. 使用指针访问数组

有了前面知识的支持,再结合数组的特点,我们就可以很方便的使用指针访问数组了。

#include<stdio.h>
int main()
{int arr[5] = { 0 };int* p = arr;int sz = sizeof(arr)/sizeof(arr[0]);for (int i = 0; i < sz; i++){//输入scanf("%d", p + i);//当然也可以使用 arr+i ,或是 &arr[i]}for (int i = 0; i < sz; i++){//输出printf("%d ", *(p + i));//当然,这里也可以使用其他的表达方式}return 0;
}

这个代码搞明白后,如果我们再分析一下,数组名 arr 是数组首元素的地址,可以赋值给p,其实数组名 arr 和 p 在这里是等价的。那我们可以使用 arr[i] 可以访问数组的元素,那 p 是否也可以这样访问数组呢?

#include<stdio.h>
int main()
{int arr[5] = { 0 };int* p = arr;int sz = sizeof(arr)/sizeof(arr[0]);for (int i = 0; i < sz; i++){//输入scanf("%d", p + i);//当然也可以使用 arr+i ,或是 &arr[i]}for (int i = 0; i < sz; i++){//输出//printf("%d ", *(p + i));printf("%d ", p[i]);//当然,这里也可以使用其他的表达方式}return 0;
}

这个代码和上面的代码结果是完全相同的,这也就是说本质上 p[i] 是等价于 * (p+i) 的。

同理arr[i 应该等价于 *(arr+i) ,数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的

5. 一维数组传参的本质

数组我们之前也讲过了,数组是可以传递给函数的,这个小节我们讨论一下数组传参的本质。
首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给一个函数后,函数内部求数组的元素个数吗?

我们来测试一下:

#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;
}

输出结果
我们发现在函数内部是没有正确获得数组的元素个数。
这就要了解数组传参的本质了,上个小节中讲过:数组名是数组首元素的地址,那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组首元素的地址。
所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写 sizeof(arr) 计算的是一个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

补充:

void test(int arr[])//参数写成数组形式,本质上还是指针
{printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩
}
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };test(arr);return 0;
}

也就是说:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

6. 冒泡排序

冒泡排序是一种排序算法,可以将乱序的数组排序成降序(或升序)。
冒泡排序的核心思想就是:两两相邻的元素进行比较
在比较之后,如果这两个元素的排序不符合最终要求的结果(降序或升序),那就进行交换。

那要比较多少次呢?
在最坏情况下,每次从头到尾遍历一遍数组,可以将一个数字送至它最终的位置上,比如说降序,第一次可以将最小的数字送至最后一位,那么在下一次遍历时,最后一位就不用参与比较了,同时,在遍历了 sz-1 (sz为数组元素个数)次后,最后剩下那个没排序的元素也一定已经在它应该在的位置了。

代码例子:

#include<stdio.h>
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{for (int i = 0; i < sz - 1; i++)//外部循环,控制遍历次数{								//循环 sz-1 次,因为最后一个元素可以不进行排序for (int j = 0; j < sz - i - 1; j++)//内层循环,控制每次遍历中循环次数{									//循环 sz-i-1 次,是因为每次外层循环会将一个元素//送到正确的位置上,这些元素不用再进行排序if (arr[j] > arr[j + 1])//从这里可以看出,这个冒泡排序是是升序的{int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;//交换两个变量}}}
}int main()
{int arr[] = { 3,1,7,5,8,9,0,2,4,6 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz);for (int i = 0; i < sz; i++){printf("%d ", arr[i]);}return 0;
}

再想一想,这个算法还有可以优化的地方吗?
如果说在进行了几次排序或是还没进行排序,数组中的元素就已经是有序的了,那后面的排序还需要进行吗?
我们来看优化后的代码:

void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{for (int i = 0; i < sz - 1; i++){int flag = 1;//假设这⼀趟已经有序了for (int j = 0; j < sz - i - 1; j++){if (arr[j] > arr[j + 1]){flag = 0;//发生交换就说明无序int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}if (flag == 1)//这⼀趟没交换就说明已经有序,后续无需排序了break;}
}
int main()
{int arr[] = { 3,1,7,5,8,9,0,2,4,6 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz);for (int i = 0; i < sz; i++){printf("%d ", arr[i]);}return 0;
}

冒泡排序的时间复杂度:O(n),空间复杂度:O(1)

7. 二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
那就是 二级指针
比如说这个代码

#include<stdio.h>
int main()
{int a = 10;int* pa = &a;int** pa = &pa;//二级指针取出指针变量的地址return 0;
}

二级指针
二级指针的运算:

  1. *ppa 通过对 ppa 中的地址进行解引用,这样找到的是 pa,*ppa 其实访问的就是 pa 。
int b = 20;
*ppa = &b;//等价于 pa = &b;
  1. **ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作,*pa ,那找到的是 a 。
** ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

8. 指针数组

指针数组是指针还是数组?
我们类比一下,整型数组,是存放整型的数组,字符数组是存放字符的数组。
那指针数组呢?就是是存放指针的数组

整形数组与字符数组
指针数组
指针数组的每个元素又是地址,可以指向一块区域。

我们来分析一下上面的指针数组为什么写作int * arr[5],我们知道,整形数组写作int arr[5],前面的 int 是什么?自然是数组存放的变量的类型,那指针的类型(以整形指针为例)是 int*,那么指针数组就写作这样了。

9. 指针数组模拟实现二维数组

了解了指针数组,我们不妨来做一个小测试。
我们知道二维数组:

	int arr[3][5];

这个二维数组有三行五列,那我们只需要一个有3个元素,每个元素指向含有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数组
parr[i ]是访问 parr 数组的元素,parr[i] 找到的数组元素指向了整型一维数组,parr[i][j] 就是整型一维数组中的元素。
上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的。】

谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会尽快更新完毕指针全系列!(大概为4~5篇博客)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/46406.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言 ——— 将一句英语短句中的单词进行倒置

目录 题目要求 代码实现 题目要求 将一句英语短句中的单词进行倒置&#xff0c;标点符号不倒置 如&#xff1a; 输入&#xff1a;"I like chongqing very much," 输出&#xff1a;"much, very chongqing like I" 代码实现 #include<stdio.h> #i…

MyBatis where标签替换WHERE 1 = 1会提升性能吗

MyBatis <where>标签替换WHERE 1 1会提升性能吗 查看项目早期数据库查询语句时&#xff0c;发现很多地方写了WHERE 1 1&#xff0c;怀疑这里有性能损失&#xff0c;想替换成<where>标签。 验证 已知索引 CREATE INDEX BP_LOG_BP_DATE_IDX ON QXX.BP_LOG (BP_…

java实战项目-学生管理系统(附带全套源代码及其登录注册功能的实现)--《进阶篇》

一、前言 新增了登录注册的功能&#xff0c;代码量可能会有点大&#xff0c;所有代码加起来差不多560行。这个项目对于小白来说肯定是一大难关了。文章中的每张图都是作者亲手绘制的&#xff0c;简单明了&#xff0c;如果大家认同作者&#xff0c;希望可以支持一下作者。全套源…

论文翻译 | LEAST-TO-MOST: 从最少到最多的提示使大型语言模型中的复杂推理成为可能

摘要 思维链提示&#xff08;Chain-of-thought prompting&#xff09;在多种自然语言推理任务上展现了卓越的性能。然而&#xff0c;在需要解决的问题比提示中展示的示例更难的任务上&#xff0c;它的表现往往不佳。为了克服从简单到困难的泛化挑战&#xff0c;我们提出了一种新…

快速在springboot项目中应用EasyExcel

目录 ​编辑一、介绍 二、准备工作 三、初始化项目 3.1 创建项目 3.2 引入依赖 3.3 创建数据库和数据表 3.4 编写application.properties文件 四、开始编写 4.1 目录结构 4.2 MyBatis配置类 4.3 用户实体类 4.4 mapper接口 4.5 编写数据操作文件 4.6 编写Service 业…

【Linux】03.权限

一、权限的概念 Linux下有两种用户&#xff1a;超级用户&#xff08;root&#xff09;、普通用户。 超级用户&#xff1a;可以在 linux 系统下做任何事情&#xff0c;不受限制普通用户&#xff1a;在linux下做有限的事情超级用户的命令提示符是“#”&#xff0c;普通用户的命…

字节算法岗面试,效率贼高

最近这一两周看到不少互联网公司都已经开始秋招提前批了。 不同以往的是&#xff0c;当前职场环境已不再是那个双向奔赴时代了。求职者在变多&#xff0c;HC 在变少&#xff0c;岗位要求还更高了。 最近&#xff0c;我们又陆续整理了很多大厂的面试题&#xff0c;帮助一些球友…

python--实验 11 模块

目录 知识点 模块基础 模块使用方式 自定义模块示例 模块的有条件执行 Python包结构 定义和导入包 常用第三方库及安装 实例代码 第三方库自动安装脚本 Python标准库介绍 PyInstaller 小结 实验 1.(基础题)制作文本进度条。 2.(基础题) 蒙特卡罗方法计算圆周率…

[GXYCTF2019]BabySQli

原题目描述&#xff1a;刚学完sqli&#xff0c;我才知道万能口令这么危险&#xff0c;还好我进行了防护&#xff0c;还用md5哈希了密码&#xff01; 我看到是个黑盒先想着搞一份源码 我dirsearch明明扫到了.git&#xff0c;算了直接注入试试看 随便输入了两个东西&#xff0c…

Type-C PD芯片:引领充电技术的新纪元

随着科技的飞速发展&#xff0c;人们对电子设备的依赖日益加深&#xff0c;对充电速度、效率和安全性的要求也越来越高。在这样的背景下&#xff0c;Type-C PD&#xff08;Power Delivery&#xff09;芯片应运而生&#xff0c;以其高效、安全、智能的特点&#xff0c;成为了充电…

vscode编译环境配置-golang

1. 支持跳转 如果单测函数上方不显示run test | debug test&#xff0c;需要安装Code Debugger&#xff08;因为以前的go Test Explorer不再被维护了&#xff09; 2. 单测 指定单个用例测试 go test -v run TestXXXdlv 调试 需要安装匹配的go版本和delve版本&#xff08;如…

Linux多线程编程-生产者与消费者模型详解与实现(C语言)

1.什么是生成者与消费者模型 生产者-消费者模型是并发编程中的经典问题&#xff0c;描述了多个线程&#xff08;或进程&#xff09;如何安全、有效地共享有限的缓冲区资源。在这个模型中&#xff0c;有两种角色&#xff1a; 生产者&#xff08;Producer&#xff09;&#xff1…

<数据集>光伏板缺陷检测数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;2400张 标注数量(xml文件个数)&#xff1a;2400 标注数量(txt文件个数)&#xff1a;2400 标注类别数&#xff1a;4 标注类别名称&#xff1a;[Crack,Grid,Spot] 序号类别名称图片数框数1Crack8688922Grid8248843S…

研究生发表期刊/会议必看,一文看懂A/B/C类和顶刊

主要看&#xff1a;中国计算机学会&#xff08;CCF&#xff09;推荐国际学术期刊 里面的划分等级 等级为&#xff1a;A类&#xff08;最难&#xff09;>B类&#xff08;中等难度&#xff09;>C类&#xff08;难度一般&#xff09; 本人这边计划&#xff1a;最低发C刊&a…

css-grid布局(栅格布局)

css新世界-auto-fit grid 一个比flex更强大的布局,适合做整体布局 grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); auto-fit的话有strech效果gap 不仅可以用于grid 也可用flex. 在grid-template-areas表示这个位置空着grid area 的 [a b]命名可重复命名 表示的…

双向收发的信号应该在哪进行串联端接?分享几个实用设计方法!

高速先生成员--黄刚 经过上次高速先生的描述&#xff0c;相信大家已经掌握了串联端接的秘诀了&#xff0c;简单来说&#xff0c;那就是第一步&#xff1a;先看看芯片的驱动内阻&#xff0c;第二步&#xff1a;再用加起来50欧姆匹配的方法来选择适合的串阻值&#xff0c;第三步&…

【C语言报错已解决】格式化字符串漏洞(Format String Vulnerability)

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 引言&#xff1a;一、问题描述&#xff1a;1.1 报错示例&#xff1a;1.2 报错分析&#xff1a;1.3 解决思路&#xff…

HEROIC FANTASY WERE CREATURES PACK VOL 2 (幻想生物)

这个包收集了5种英雄幻想生物:狼人,狼人,狼人山羊,狼人鲨鱼和狼人蜘蛛。 狼人:27.5 Ktris Max//101个骨骼//4种材质//最多4096*4096个纹理//40个动画(11个是根运动变体) 狼人:15.83 Ktris//66个骨骼//3种材质//最多4096*4096个纹理//35个动画(9个是根运动变体) wereg…

Centos 使用nfs配置共享目录使docker集群所有容器日志统一主机访问

Centos 使用nfs配置共享目录&#xff0c;使docker集群所有容器日志统一存放在主机一个共享目录下&#xff0c;供开发人员访问查看 准备两台或以上Centos服务器 192.168.0.1 nfs服务器 192.168.0.2 nfs客户端 以root用户登录192.168.0.1服务器&#xff0c;执行以下操作 注意先…

excel根据数据批量创建并重命名工作表

需求 根据一列数据&#xff0c;批量创建并重命名工作表 做法 1. 右键该sheet&#xff0c;选择查看代码 2. 输入VBA代码 正向创建 Sub create_sheets_by_col()Dim num% 定义为integer*num Application.WorksheetFunction.CountA(Sheet1.Range("A:A")) num是非空…