当前位置: 首页 > news >正文

C++ const、volatile和mutable关键字详解

对于cvconst volatile)类型限定符和关键字mutable在《cppreference》中的定义为:

cv可出现于任何类型说明符中,以指定被声明对象或被命名类型的常量性(constness)或易变性(volatility)。

  • const----------定义类型为常量类型。

  • volatile--------定义类型为易变类型。

mutable用于指定不影响类的外部可观察状态的成员(通常用于互斥体、记忆缓存、惰性求值和访问指令等)。

  • mutable------容许常量类类型对象修改相应类成员。

const

const实际上是一个类型说明,告诉编译器const修饰的目标是不变的,允许编译器对其进行额外的优化,如果后面代码不小心修改它了,就编译失败,告诉用户该目标被意外修改了,提高程序的安全性和可控性。

const修饰普通变量

const修饰过的变量,编译器往往将其作为一个常量进行处理,同时,const修饰的变量在编译阶段会被编译器替换为相应的值,来提高程序的执行效率。

#include <iostream>using namespace std;int main() {const int i = 50;          // 普通常量const static int si = 50;  // 静态常量int* p_int = (int*)&i;  // 强制类型转换为int*int* p_sint = (int*)&si;*p_int = 100;  // 通过非常常量指针修改常量i的值,该行为是C++为未定义行为//*p_sint = 100;//编译不会报错,程序运行时崩溃,且该行为也是C++为未定义行为cout << "i:" << i << ", i的地址: " << &i << endl;//编译器阶段会将常量i替换为50cout << "*p_int:" << *p_int << ", *p_int的地址: " << p_int << endl;return 0;
}

:类型是 const修饰的对象,或常量对象的非可变子对象。这种对象不能被修改:直接尝试这么做是编译时错误,而间接尝试这么做(例如通过到非常量类型的引用或指针修改常量对象)的行为未定义。

输出结果:

i:50, i的地址: 0x7fffffffd9d4
*p_int:100, *p_int的地址: 0x7fffffffd9d4

i*p_int打印出的地址都是0x7fffffffd9d4可以看出,我们偷偷修改i的值成功了(但该行为是C++未定义的行为),但是为何i*p_int的结果却是不同的,这就从侧面证实了const常量具有宏替换的特性,即程序在编译阶段就会其进行部分的替换,例如上述例子中对语句

cout << "i:" << i << ", i的地址: " << &i << endl;

在编译阶段替换为

cout << "i:" << 50 << ", i的地址: " << &i << endl;

因此导致我们输出的i的值为50

同时,当我们想要通过使用非常量指针修改静态常量si时候,编译通过,但是在运行过程中导致程序崩溃(这就是不按规矩办事的后果,使用了C++未定义的行为,编译器也帮不了我们,最终导致程序挂掉)。

const的内部链接性

通常情况下,在未声明为 extern 的变量声明上使用 const 限定符,会给予该变量内部连接(即名称在用于声明变量的文件的外部是不可见的)的特性。即与static类似(详情可参考《C++避坑—关键字static的使用及注意事项》),但是与其不同的是其可以通过extern来改变其内部连接性。

const修饰指针和引用

/********指针********/
//指向const对象的指针
const int* p1; //const修饰的是int,表示p1指向的内容不能被修改
int const* p2; //const修饰的是int,表示p2指向的内容不能被修改//指向对象的const指针
int* const p3; //const修饰的是*,表示p3的指向不能被修改//指向const对象的const指针
const int* const p4; //第一个修饰的是int,第二个修饰的是*,表示p4指向的内容和p4的指向都不能被修改
const int const* p5; //同上,表示p5指向的内容和p5的指向都不能被修改/*注:从上面我们可以总结出来的一个规律:
const优先修饰其左边最近的类型,
如果左边没有,就修饰右边离他最近的那个*//********引用********/
const int a = 0;
//由于a1引用a之后,不能引用其他实体,所以对于const int&可以看作是const int* const
const int& a1 = a; int b = 0;
const int& b1 = b;//C++允许无限定类型的引用/指针能被转换成到 const 的引用/指针

const在类中的应用

非静态数据成员可以被cv限定符修饰,这些限定符写在函数声明中的形参列表之后。其中,带有不同cv限定符(或者没有)的函数具有不同的类型,它们可以相互重载。在具有cv限定符的成员函数内,*this拥有同向的cv限定。例子如下:

#include <iostream>using namespace std;class A {public:A(int a) : a(a){};void show() { cout << "void show(): " << a << endl; }void show() const { cout << "void show() const: " << a << endl; }  // 重载/*这里void show() 等价于 void show(A* this)void show() const 等价于 void show(const A* this)因此该成员函数show()可以被重载*/void set_a(int n) { a = n; }/*void set_a(int n) const{a = n;//此时*this拥有const限制,导致a不能够被修改}//程序报错*/// void print_a() const 等价于 void print_a(const A* this)void print_a() const { cout << "print_a() const: " << a << endl; }private:int a;
};int main() {A a1(1);const A a2(2);a1.show();a2.show();a1.print_a();  // 非const对象可以调用const成员函数// 根本原因是A* this可以隐式转换const A* this// 最终调用void print_a(const A* this)// 即void print_a() consta2.print_a();a1.set_a(2);//   a2.set_a(1);  // 编译报错,const对象不能调用非const成员函数,根本原因是根本原因是const A* this可以隐式转换A* thisreturn 0;
}

输出结果:

void show(): 1
void show() const: 2
print_a() const: 1
print_a() const: 2

对于上述例子我们可以得出:

  • const对象不能调用非const成员函数。 => const成员函数内部不能调用其他非cosnt成员函数。
  • const对象可以调用const成员函数。=> 非cosnt成员函数可以调用其他cosnt成员函数。

volatile

volatile 主要作用是告诉编译器其修饰的目标是易变的,在编译的时候不要去优化它(例如读取的时候,都从目标内存地址读取),防止编译器误认为某段代码中目标是不会被改变,而造成过度优化。

:编译器大部分情况是从内存读取变量的值的,但有时候编译器认为在某段代码中,某个变量的值是没有变化的,所以认为寄存器里的值跟内存里是一样的,为了提高读取速度,编译器可能会从寄存器中读取变量,但是在某些情况下变量的值被其他元素(如另外一个线程或者中断服务)修改,这样导致程序读取变量的值不是最新的,产生异常。

因此,volatile 关键字对于声明共享内存中可由多个进程访问的对象或用于与中断服务例程通信的全局数据区域很有用。如果某个目标被声明为 volatile,则每当程序访问它的时候,编译器都会重新加载内存中的值。 这虽然降低了目标的读取速度,但是保证了目标读取的正确性,这也是保证我们程序可预见性的唯一方法。

下面我们通过一个读取系统时间的例子来看一下volatile在实际开发过程中的应用:

#include <iostream>
#include <ctime>
#include <unistd.h>
// #include <windows.h> //win下为该头文件using namespace std;
int main()
{const time_t time_val = 0; time_t *p_time_t = const_cast<time_t *>(&time_val);time(p_time_t);cout << time_val << endl;// 休眠1ssleep(1); // linux下sleep函数,单位为秒// Sleep(1000); // win下的sleep函数,单位为毫秒time(p_time_t);cout << time_val << endl;return 0;
}

time函数在ctime头文件中定义,其主要作用是获取系统当前时间,其原型如下:

std::time_t time( std::time_t* arg );

返回编码为std::time_t对象的当前日历时间,并将它存储于 arg 所指向的对象,除非 arg 是空指针。

输出结果:

0
0

很明显结果不符合我们的预期,具体原因就是我们上面介绍的const常量具有宏替换的特性,编译器认为这段可以更好的优化,在编译阶段就对其进行了替换。那我们如何修改才能达到我们的实现呢?对,就是添加volatile关键字修饰,具体实现如下:

#include <iostream>
#include <ctime>
#include <unistd.h>
// #include <windows.h> //win下为该头文件using namespace std;
int main()
{volatile const time_t time_val = 0;time_t *p_time_t = const_cast<time_t *>(&time_val);time(p_time_t);cout << time_val << endl;// 休眠1ssleep(1); // linux下sleep函数,单位为秒// Sleep(1000); // win下的sleep函数,单位为毫秒time(p_time_t);cout << time_val << endl;return 0;
}

输出结果:

1680339937
1680339938

从输出结果看出,结果符合我们的预期。

这时候你可能会有疑问:volatile const是什么鬼?const表示time_val是常量,volatile表示time_val是可变的,难道是易变的常量?这不是矛盾吗?

这里我们确实可以将time_val成为易变的常量,只不过常量(不可修改)意味着time_val在其作用域中(这里指的是main函数中)是不可以被改变的,但是在其作用域外面(这里指的是time()函数内)是可以被改变的。volatile const其实是在告诉编译器,在main()函数内,time_valconst的,你帮我看点,我不能随意的修改,但这个值在作用域外可能会被其他东西修改,这玩意也是volatile的,你编译的时候也别优化它了,在每次读取的时候,也老老实实从它的存储位置重新读取吧。

注:volatile constconst volatile是一样的,都代表易变的常量。

volatile修饰常量、指针和引用

volatile修饰常量指针和引用的使用方法域const类似,这里不做过多的解释,但需要注意的是volatile没有像const的内部链接属性。

volatile修饰函数的参数

int sequare(volatile int* ptr)
{return *ptr * *ptr;
}

上述例子是为了计算一个易变类型的int的平方,但是函数内部实现存在问题,因为

return *ptr * *ptr;

其处理逻辑类似下面的情况:

int a = *ptr; 
int b = *ptr;
return a * b;

由于*ptr是易变的,因此ab获取的值可能是不一样的,因此最好采用如下的方式:

int sequare(volatile int* ptr)
{int a = *ptr;return a * a;
}

mutable

mutable主要是为了突破const的某些限制而设定的,即允许常量类型对象相应的类成员可以被修改,其常在非引用非常量类型的非静态数据成员中出现。

在上面的介绍中,我们知道在在获取类某些状态的成员函数中,如果不涉及状态的变更,我们一般会将成员函数声明成const,这将意味着在该函数中,所有的成员函数都不可以被修改,但有些时候我们需要在该const函数中修改一些跟类状态无关的数据乘员,那么这时候就需要mutable发挥作用了,即将该需要修改的成员使用mutable修饰。

#include <iostream>
using namespace std;class A
{
public:A(int data = 0) : int_data(data), times(0) {}void show() const{times++; //因为times被mutable修饰,突破了const的限制cout << "data : " << int_data << endl;}int getNumOfCalls() { return times; }private:int int_data;mutable int times;
};int main()
{A a(1);cout << "a的show()被调用了:" << a.getNumOfCalls() << "次。" << endl;a.show();cout << "a的show()被调用了:" << a.getNumOfCalls() << "次。" << endl;return 0;
}

输出结果:

a的show()被调用了:0次。
data : 1
a的show()被调用了:1次。

上例void show()const修饰后,导致在该函数内类成员不能被修改,但由于timesmutable修饰后,突破了const的限制,使得times在该函数内部可以被修改。

mutable的另一个应用场景就是用来移除lambda函数中按复制捕获的形参的const限制。通常情况下(不提供说明符),复制捕获的对象在lambda体内是 const 的,并且在其内部无法修改被捕获的对象,具体的例子如下:

#include <iostream>
using namespace std;int main() {int a = 0;const int b = 0;auto f1 = [=]() {/*a++;  // 错误,不提供说明符时复制捕获的对象在 lambda 体内是 const 的。b++;  // 错误,同上,且按值传递const也会传递进来*/return a;};auto f2 = [=]() mutable {  // 提供mutable说明符a++;                     // 正确,mutable解除const限制。/*b++;  // 错误,mutable无法突破b本身的const限制*/return a;};cout << a << ", " << b << endl;        // 输出0, 0cout << f1() << ", " << f2() << endl;  // 输出0, 1return 0;
}

总 结

const主要用来告诉编译器,被修饰的变量是不变类型的,在有些情况下可以对其进行优化,同时如果后面代码不小心修改了,编译器在编译阶段报错。在类的应用中,const对象不能调用非const成员函数,非const对象可以调用const成员函数。

volatile主要用来告诉编译器,被修饰变量是易变,在编译的时候不要对其进行优化,读取它的时候直接从其内存地址读取。

同时,constvolatile限定的引用和指针支持下列的隐式转换:

  • 无限定类型的引用/指针能被转换成const的引用/指针
  • 无限定类型的引用/指针能被转换成volatile 的引用/指针
  • 无限定类型的引用/指针能被转换成const volatile 的引用/指针
  • const 类型的引用/指针能被转换成const volatile 的引用/指针
  • volatile 类型的引用/指针能被转换成const volatile 的引用/指针

对于const修饰的成员函数内类成员const的属性,可以通过使用对mutable来解除const限制。同样的,mutable也可以用来移除lambda函数中按复制捕获的形参的const限制。

文章首发公众号:iDoitnow 如果喜欢话,可以关注一下

相关文章:

C++ const、volatile和mutable关键字详解

对于cv&#xff08;const 与 volatile&#xff09;类型限定符和关键字mutable在《cppreference》中的定义为&#xff1a; cv可出现于任何类型说明符中&#xff0c;以指定被声明对象或被命名类型的常量性&#xff08;constness&#xff09;或易变性&#xff08;volatility&#…...

MySQL实验四:数据更新

MySQL实验四&#xff1a;数据更新 目录MySQL实验四&#xff1a;数据更新导读表结构sql建表语句模型图1、 SQL更新&#xff1a;将所有学生的年龄增加1岁代码2、SQL更新&#xff1a;修改“高等数学”课程倒数三名成绩&#xff0c;在原来分数上减5分代码解析3、SQl更新&#xff1a…...

商汤科技推出“日日新SenseNova”,大模型体系赋能人工智能新未来

2023年4月10日&#xff0c;商汤科技SenseTime技术交流日活动在上海举行&#xff0c;分享了以“大模型大算力”推进AGI&#xff08;通用人工智能&#xff09;发展的战略布局&#xff0c;并公布了商汤在该战略下的“日日新SenseNova”大模型体系。 公开信息显示&#xff0c;商汤科…...

【中创AI】斯坦福人工智能年度报告:AI论文发表量中国世界第一!

斯坦福以人为本人工智能研究所 (HAI) 发布了最新一期的 2023 AI 指数 (2023 AI Index) 报告&#xff0c;探讨了过去一年机器学习的发展。 &#xff08;斯坦福HAI于2019年初成立&#xff0c;致力于研究新的AI方法&#xff0c;并研究该技术对社会的影响。其每年发布一份AI指数报…...

Java基础(五)面向对象编程(基础)

学习面向对象内容的三条主线 Java类及类的成员&#xff1a;&#xff08;重点&#xff09;属性、方法、构造器&#xff1b;&#xff08;熟悉&#xff09;代码块、内部类面向对象的特征&#xff1a;封装、继承、多态、&#xff08;抽象&#xff09;其他关键字的使用&#xff1a;…...

寻找CSDN平行世界的另一个你

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 寻找CSDN平行世界的另一个你摘要前言列表测试目的摘要 本文作了一个测试&#xff0c;看看在 CSDN 的博文中&#xff0c;艾特&#xff08;&#xff09;某个好友&#xff0c;TA是否能够…...

ChatGPT的发展对客户支持能提供什么帮助?

多数组织认为客户服务是一种开销&#xff0c;实际上还可以将客户服务看成是一种机会。它可以让你在销售后继续推动客户的价值。成功的企业深知&#xff0c;客户服务不仅可以留住客户&#xff0c;还可以增加企业收入。客户服务是被低估的手段&#xff0c;它可以通过推荐、见证和…...

数据安全评估体系建设

数据安全评估是指对重要数据、个人信息等数据资产的价值与权益、合规性、威胁、脆弱性、防护等进行分析和判断&#xff0c;以评估数据安全事件发生的概率和可能造成的损失&#xff0c;并采取相应的措施和建议。 数据安全评估的重要性和背景 1.国家法律法规下的合规需要 目前数…...

论文阅读 - ANEMONE: Graph Anomaly Detection with Multi-Scale Contrastive Learning

目录 摘要 1 简介 2 问题陈述 3 PROPOSED ANEMONE FRAMEWORK 3.1 多尺度对比学习模型 3.1.1 增强的自我网络生成 3.1.2 补丁级对比网络 3.1.3 上下文级对比网络 3.1.4 联合训练 3.2 统计异常估计器 4 EXPERIMENTS 4.1 Experimental Setup 4.1.1 Datasets 4.1.2 …...

数据密集型应用存储与检索设计

本文内容翻译自《数据密集型应用系统设计》&#xff0c;豆瓣评分高达 9.7 分。 什么是「数据密集型应用系统」&#xff1f; 当数据&#xff08;数据量、数据复杂度、数据变化速度&#xff09;是一个应用的主要挑战&#xff0c;那么可以把这个应用称为数据密集型的。与之相对的是…...

Spring Boot集成Redis实现keyspace监听 | Spring Cloud 34

一、前言 在前面我们通过以下章节对Redis的keyevent&#xff08;键事件通知&#xff09;使用有了基础的了解&#xff1a; Spring Boot集成Redis实现keyevent监听 | Spring Cloud 33 现在开始我们正式学习Redis的keyspace&#xff08;键空间通知&#xff09;&#xff0c;在本…...

如何搭建chatGPT4.0模型-国内如何用chatGPT4.0

国内如何用chatGPT4.0 在国内&#xff0c;目前可以通过以下途径使用 OpenAI 的 ChatGPT 4.0&#xff1a; 自己搭建模型&#xff1a;如果您具备一定的技术能力&#xff0c;可以通过下载预训练模型和相关的开发工具包&#xff0c;自行搭建 ChatGPT 4.0 模型。OpenAI提供了相关的…...

【故障定位】基于多元宇宙算法的主动配电网故障定位方法研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

基于html+css的自适应展示1

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…...

DolphinDB +Python Airflow 高效实现数据清洗

DolphinDB 作为一款高性能时序数据库&#xff0c;其在实际生产环境中常有数据的清洗、装换以及加载等需求&#xff0c;而对于该如何结构化管理好 ETL 作业&#xff0c;Airflow 提供了一种很好的思路。本篇教程为生产环境中 ETL 实践需求提供了一个解决方案&#xff0c;将 Pytho…...

pip3 升级软件包时遇到超时错误解决方法

如果你在使用 pip3 升级软件包时遇到超时错误&#xff0c;可能是因为下载速度缓慢或网络不稳定。以下是解决方法&#xff1a; 更改 pip3 源&#xff1a;你可以切换到其他 pip3 源&#xff0c;例如清华、阿里等等&#xff0c;以提高下载速度。 pip3 install -i https://pypi.tun…...

Linux环境开机自启动

1.制作服务 在/etc/systemd/system/路径下创建kkFile.service文件 cd /etc/systemd/system/ vim kkFile.service2.写入如下内容 [Unit] DescriptionkkFile service [Service] Typeforking ExecStart/sinosoft/yjya/kkFileView-4.0.0/bin/startup.sh [Install] WantedBymulti…...

字节8年测试经验,送给想要学习自动化测试的同学6条建议

我的职业生涯开始和大多数测试人一样&#xff0c;开始接触都是纯功能界面测试。那时候在一家电商公司做测试&#xff0c;做了有一段时间&#xff0c;熟悉产品的业务流程以及熟练测试工作流程规范之后&#xff0c;效率提高了&#xff0c;工作比较轻松&#xff0c;也得到了更好的…...

快速搭建springboot websocket客户端

一、前言WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。HTML5 定义的 WebSocket 协议&#xff0c;能更好的节省服务器资源和带宽&#xff0c;并且能够更实时地进行通讯。HTML5 定义的 WebSocket 协议&#xff0c;能更好的节省服务器资源和带宽&…...

Python 操作 MongoDB 详解

嗨害大家好鸭&#xff01;我是芝士❤ 一、前言 MongoDB属于 NoSQL&#xff08;非关系型数据库&#xff09;&#xff0c; 是一个基于分布式文件存储的开源数据库系统。 二、操作 MongoDB 1. 安装 pymongo python 使用第三方库来连接操作 MongoDB&#xff0c; 所以我们首先安…...

电脑商城--用户注册登录

用户注册 1 用户-创建数据表 1.使用use命令先选中store数据库。 USE store; 2.在store数据库中创建t_user用户数据表。 CREATE TABLE t_user (uid INT AUTO_INCREMENT COMMENT 用户id,username VARCHAR(20) NOT NULL UNIQUE COMMENT 用户名,password CHAR(32) NOT NULL COMME…...

[AI绘画]sd学习记录(二)文生图参数进阶

目录 7.高分辨率修复&#xff1a;以小博大8.细化器&#xff08;Refiner&#xff09;&#xff1a;两模型接力9.随机数种子&#xff08;Seed&#xff09;&#xff1a;复现图片吧 本文接续https://blog.csdn.net/qq_23220445/article/details/148460878?spm1001.2014.3001.5501…...

OpenCV 滑动条调整图像对比度和亮度

一、知识点 1、int createTrackbar(const String & trackbarname, const String & winname, int * value, int count, TrackbarCallback onChange 0, void * userdata 0); (1)、创建一个滑动条并将其附在指定窗口上。 (2)、参数说明: trackbarname: 创建的…...

【vue】Uniapp 打包Android 文件选择上传问题详解~

需求 uniapp兼容android app&#xff0c;pc&#xff0c;h5的文件选择并上传功能。 需要支持拍照和相册选择&#xff0c;以及选择其他类型文件上传~ 实践过程和问题 开始使用uni-file-picker组件 以为很顺利&#xff0c;android模拟器测试…… 忽略了平台兼容性提示~&#…...

ubuntu显示器未知

在Ubuntu系统中&#xff0c;当外接显示器被识别为“未知设备”时&#xff0c;可通过以下日志文件进行问题诊断&#xff0c;结合Xorg日志和内核日志综合分析&#xff1a; &#x1f50d; 一、查看Xorg显示服务日志&#xff08;核心&#xff09; Xorg日志记录了图形界面的详细事…...

基于STM32的DHT11温湿度远程监测LCD1602显示Proteus仿真+程序+设计报告+讲解视频

DHT11温湿度远程监测proteus仿真 1. 主要功能2.仿真3. 程序4. 设计报告5. 资料清单&下载链接 基于STM32的DHT11温湿度远程监测LCD1602显示Proteus仿真设计(仿真程序设计报告讲解视频&#xff09; 仿真图proteus 8.9 程序编译器&#xff1a;keil 5 编程语言&#xff1a;C…...

Python Robot Framework【自动化测试框架】简介

销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...

xcode 各版本真机调试包下载

下载地址 https://github.com/filsv/iOSDeviceSupport 使用方法&#xff1a; 添加到下面路径中&#xff0c;然后退出重启xcode /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport...

基于VMD-LSTM融合方法的F10.7指数预报

F10.7 Daily Forecast Using LSTM Combined With VMD Method ​​F10.7​​ solar radiation flux is a well-known parameter that is closely linked to ​​solar activity​​, serving as a key index for measuring the level of solar activity. In this study, the ​​…...

springboot的test模块使用Autowired注入失败

springboot的test模块使用Autowired注入失败的原因&#xff1a; 注入失败的原因可能是用了junit4的包的Test注解 import org.junit.Test;解决方法&#xff1a;再加上RunWith(SpringRunner.class)注解即可 或者把Test由junit4改成junit5的注解&#xff0c;就不用加上RunWith&…...