Qt实现单例模式:Q_GLOBAL_STATIC和Q_GLOBAL_STATIC_WITH_ARGS
目录
1.引言
2.了解Q_GLOBAL_STATIC
3.了解Q_GLOBAL_STATIC_WITH_ARGS
4.实现原理
4.1.对象的创建
4.2.QGlobalStatic
4.3.宏定义实现
4.4.注意事项
5.总结
1.引言
设计模式之单例模式-CSDN博客
所谓的全局静态对象,大多是在单例类中所见,在之前写过一篇文章详细的讲解了单例模式的UML结构、实现方式、使用场景以及注意事项等等,下面就来讲讲Qt是怎么实现单例模式的,以及Qt实现单例模式怎么实现"dead-reference检测"的。Qt 提供了两个个非常方便的宏Q_GLOBAL_STATIC和Q_GLOBAL_STATIC_WITH_ARGS,可以快速创建全局静态对象。
2.了解Q_GLOBAL_STATIC
Q_GLOBAL_STATIC宏是定义在qglobalstatic.h中,这个文件的Qt源码中的位置是(以Qt5.12.12为例) 【.\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\global】,它的语法为:
Q_GLOBAL_STATIC(Type, VariableName)
其中Type为数据类型,VariableName为变量的名称。 它主要用于创建跨越多个文件的全局静态对象。其主要作用在于两点:
1)懒惰初始化(Lazy initialization):它确保全局静态对象只有在首次使用时才被创建,而不是在程序启动时立即创建,从而可以减少程序启动时的初始化开销。
2)线程安全(Thread safety):在多线程环境中,Q_GLOBAL_STATIC 保证了全局静态对象的初始化是线程安全的,即使多个线程试图同时第一次访问它,对象也只会被创建一次。
下面是一个使用 Q_GLOBAL_STATIC 的示例:
#include <QMutex>
#include <QDebug>
#include <QCoreApplication>// 定义一个全局的互斥锁,用于跨线程同步访问
struct GlobalMutex {QMutex mutex;
};Q_GLOBAL_STATIC(GlobalMutex, globalMutex)int main(int argc, char *argv[]) {QCoreApplication a(argc, argv);// 当需要使用这个全局互斥锁时globalMutex()->mutex.lock();qDebug() << "Doing some thread-safe operation...";globalMutex()->mutex.unlock();return a.exec();
}
在这里例子中,定义了一个 GlobalMutex 结构体,包含一个 QMutex 对象。然后使用 Q_GLOBAL_STATIC 宏来创建一个全局静态的 GlobalMutex 实例,命名为 globalMutex。这个互斥锁可以在程序的任何地方使用,并保证只在首次使用时被初始化,同时保证了其初始化过程是线程安全的。
使用 Q_GLOBAL_STATIC 的好处是它避免了程序中手动管理全局变量初始化顺序的复杂度,也消除了"SIOF - Static Initialization Order Fiasco"(静态初始化顺序问题)的风险,因为静态对象仅在首次访问时被创建,避免了因依赖其他全局对象在初始化时还未创建导致的问题。同时,当全局对象具有复杂的构造和析构过程时,使用 Q_GLOBAL_STATIC 可以确保安全地创建和清理资源。
“SIOF - Static Initialization Order Fiasco”(静态初始化顺序问题)指的是在C++程序中,不同编译单元(通常是不同的源文件)中全局(或静态)对象的初始化顺序是未定义的。
也就是说,如果有两个全局静态对象,一个位于文件A中,另一个位于文件B中,且对象A在其初始化过程中依赖对象B,那么就存在一个问题:在主函数 main() 开始执行之前,无法保证对象B一定在对象A之前被初始化。如果对象A在它的构造函数中访问了对象B,而对象B还没有被初始化,这可能会导致未定义的行为,比如访问无效的内存,导致程序崩溃等问题。
Q_GLOBAL_STATIC 通过懒加载模式解决了这个问题。当首次使用全局对象时,这个对象才会被创建,并且这个创建过程是线程安全的。这意味着无论全局对象的定义在哪个编译单元中,它们都将在实际使用时才被初始化,而不是在程序启动时。
这样一来,就消除了因为静态初始化顺序引起的未定义行为。任何一个全局对象在实际被使用前都不会被初始化,因此,它们的初始化过程可以安全地引用其他全局对象,不会由于它们尚未初始化而出错。只要对象的使用顺序正确,它们的依赖关系就可以正常工作,因为实际使用时所依赖的对象已经被创建了。
3.了解Q_GLOBAL_STATIC_WITH_ARGS
Q_GLOBAL_STATIC_WITH_ARGS的语法为:
Q_GLOBAL_STATIC_WITH_ARGS(Type, VariableName, Arguments)
其中Type为数据类型,VariableName为变量的名称,Arguments是Type的构造函数参数。 它是 Q_GLOBAL_STATIC 的一个变体,它允许使用参数来初始化全局静态对象。这意味着当全局静态对象需要在构造函数中传递一些参数来初始化时,Q_GLOBAL_STATIC_WITH_ARGS 就特别有用。
示例如下:
#include <QString>
#include <QCoreApplication>// 假设这是一个需要参数初始化的类
class Logger {
public:Logger(QString logFileName) {// 假设使用这个文件名初始化日志系统_logFileName = logFileName;}void log(const QString &message) {// 假设记录日志到文件}private:QString _logFileName;
};// 使用指定的日志文件名初始化全局日志对象
Q_GLOBAL_STATIC_WITH_ARGS(Logger, globalLogger, (QString("application.log")))int main(int argc, char *argv[]) {QCoreApplication app(argc, argv);// 使用全局日志对象记录一条消息globalLogger()->log("Application started");return app.exec();
}
在这个例子中,Logger 类是一个日志记录器,它通过构造函数接收一个日志文件名来初始化。使用 Q_GLOBAL_STATIC_WITH_ARGS 宏创建了一个全局的 Logger 实例 globalLogger,并通过传递了一个参数 "application.log" 作为日志文件名进行初始化。
然后,在 main 函数中,使用 globalLogger() 来获取全局日志实例并记录一条消息,这与前面的 Q_GLOBAL_STATIC 示例类似。全局的 Logger 实例会在首次使用时进行懒惰初始化,并保证初始化的线程安全性。
通过这种方式,Q_GLOBAL_STATIC_WITH_ARGS 引入了构造函数参数,提供了更多的灵活性,用于初始化那些需要额外信息才能正确创建的全局静态对象。
4.实现原理
4.1.对象的创建
Qt根据不同的平台实现了两个方式,一种是静态方式,类似静态局部变量,源码如下:
#define Q_GLOBAL_STATIC_INTERNAL(ARGS) \Q_GLOBAL_STATIC_INTERNAL_DECORATION Type *innerFunction() \{ \struct HolderBase { \~HolderBase() Q_DECL_NOTHROW \{ if (guard.load() == QtGlobalStatic::Initialized) \guard.store(QtGlobalStatic::Destroyed); } \}; \static struct Holder : public HolderBase { \Type value; \Holder() \Q_DECL_NOEXCEPT_EXPR(noexcept(Type ARGS)) \: value ARGS \{ guard.store(QtGlobalStatic::Initialized); } \} holder; \return &holder.value; \}
另外一种是通过new的方式创建,源码如下:
#define Q_GLOBAL_STATIC_INTERNAL(ARGS) \Q_DECL_HIDDEN inline Type *innerFunction() \{ \static Type *d; \static QBasicMutex mutex; \int x = guard.loadAcquire(); \if (Q_UNLIKELY(x >= QtGlobalStatic::Uninitialized)) { \QMutexLocker locker(&mutex); \if (guard.load() == QtGlobalStatic::Uninitialized) { \d = new Type ARGS; \static struct Cleanup { \~Cleanup() { \delete d; \guard.store(QtGlobalStatic::Destroyed); \} \} cleanup; \guard.storeRelease(QtGlobalStatic::Initialized); \} \} \return d; \}
这里创建对象之前也是经过了双重条件判断的,只是一般单实例模式的实现是双重检测指针,这里是guard的双重状态监测;这里还用到了一个小技巧,定义了静态局部变量Cleanup,利用它的析构函数自动释放刚刚创建的Type。
4.2.QGlobalStatic
它的源码如下:
template <typename T, T *(&innerFunction)(), QBasicAtomicInt &guard>
struct QGlobalStatic
{typedef T Type;bool isDestroyed() const { return guard.load() <= QtGlobalStatic::Destroyed; }bool exists() const { return guard.load() == QtGlobalStatic::Initialized; }operator Type *() { if (isDestroyed()) return 0; return innerFunction(); }Type *operator()() { if (isDestroyed()) return 0; return innerFunction(); }Type *operator->(){Q_ASSERT_X(!isDestroyed(), "Q_GLOBAL_STATIC", "The global static was used after being destroyed");return innerFunction();}Type &operator*(){Q_ASSERT_X(!isDestroyed(), "Q_GLOBAL_STATIC", "The global static was used after being destroyed");return *innerFunction();}
};
QGlobalStatic实现了创建对象的访问;如果在程序生命周期中从未使用该对象,除了QGlobalStatic :: exists()和QGlobalStatic :: isDestroyed()函数外,类型Type的内容将不会创建,并且不会有任何退出时间操作。
如果该对象被创建,它将在退出时被销毁,类似于C atexit函数。在大多数系统中,事实上,如果在退出之前将库或插件从内存中卸载,也会调用析构函数。
由于销毁是在程序退出时发生的,因此不提供线程安全性。这包括插件或库卸载的情况。另外,由于析构函数不会抛出异常,因此也不会提供异常安全性。
但是,重新调用是允许的,在销毁期间,可以访问全局静态对象,并且返回的指针与销毁开始之前的指针相同。销毁完成后,不允许访问全局静态对象,除非在QGlobalStatic API中注明。
4.3.宏定义实现
源码如下:
#define Q_GLOBAL_STATIC_WITH_ARGS(TYPE, NAME, ARGS) \namespace { namespace Q_QGS_ ## NAME { \typedef TYPE Type; \QBasicAtomicInt guard = Q_BASIC_ATOMIC_INITIALIZER(QtGlobalStatic::Uninitialized); \Q_GLOBAL_STATIC_INTERNAL(ARGS) \} } \static QGlobalStatic<TYPE, \Q_QGS_ ## NAME::innerFunction, \Q_QGS_ ## NAME::guard> NAME;#define Q_GLOBAL_STATIC(TYPE, NAME) \Q_GLOBAL_STATIC_WITH_ARGS(TYPE, NAME, ())
从上述代码可以看出:
1)根据不同的 NAME,生成了不同的命名空间,虽然对象创建函数、多线程同步变量guard的名字一样,但是是在不同的命名空间,因此生成的QGlobalStatic也是不一样的,其实这个也是实现技巧。
2)QBasicAtomicInt 是 原子操作,是线程安全的,它的介绍在这里就不在赘述了,不明白的地方请自行查阅。
3)Q_GLOBAL_STATIC是Q_GLOBAL_STATIC_WITH_ARGS的特例。
4.4.注意事项
如果要使用该宏,那么类的构造函数和析构函数必须是公有的才行,如果构造函数和析构函数是私有或者受保护的类型,是不能使用该宏的。
Q_GLOBAL_STATIC宏在全局范围内创建一个必须是静态的类型。无法将Q_GLOBAL_STATIC宏放在函数中(这样做会导致编译错误)。最重要的是,这个宏应该放在源文件中,千万不要放在头文件中。由于生成的对象具有静态链接,因此如果宏放置在标题中并且被多个源文件包含,该对象将被多次定义,并且不会导致链接错误。相反,每个单元将引用一个不同的对象,这可能会导致微妙且难以追踪的错误。
如果两个Q_GLOBAL_STATIC对象正在两个不同的线程上初始化,并且每个初始化序列都访问另一个线程,则可能会发生死锁。出于这个原因,建议保持全局静态构造器简单,否则,确保在构造过程中不使用全局静态的交叉依赖。
5.总结
Q_GLOBAL_STATIC 提供了一个安全的模式来创建、使用和清理全局对象,这在大型应用程序中特别有用。它简化了单例模式的使用,并且避免了手动管理全局资源带来的复杂性和风险。
推荐阅读
设计模式之单例模式
相关文章:
Qt实现单例模式:Q_GLOBAL_STATIC和Q_GLOBAL_STATIC_WITH_ARGS
目录 1.引言 2.了解Q_GLOBAL_STATIC 3.了解Q_GLOBAL_STATIC_WITH_ARGS 4.实现原理 4.1.对象的创建 4.2.QGlobalStatic 4.3.宏定义实现 4.4.注意事项 5.总结 1.引言 设计模式之单例模式-CSDN博客 所谓的全局静态对象,大多是在单例类中所见,在之前…...
通过nginx转发后应用偶发502bad gateway
序言 学习了一些东西,如何才是真正自己能用的呢?好像就是看自己的潜意识的反应,例如解决了一个问题,那么下次再碰到类似的问题,能直接下意识的去找到对应的信息,从而解决,而不是和第一次碰到一样…...
linux中如何进行yum源的挂载
linux中如何进行yum源的挂载 1.首先创建目录[rootserver /]# mkdir /rhel92.使用mount命令进行、dev/cdrom/的镜像文件进行挂载[rootserver /]# mount /dev/cdrom /rhel9/ 注意:此时设立的是临时命令。重启后则失效,若想在下次开启后仍然挂载&a…...
ffmpeg的部署踩坑及简单使用方式
ffmpeg的使用方式有以下几种: 使用原生安装包 直接在ffmpeg官网上下载安装该软件,加入到环境变量中就可以使用了 优点:简单,灵活,代码中也不用添加其他第三方的包 缺点:需要手动安装ffmpeg,这点比较麻烦 部署-windows 在windows环境下,有时就算加入到了环境变量,…...
misc刷题记录2[陇剑杯 2021]
[陇剑杯 2021]webshell (1)单位网站被黑客挂马,请您从流量中分析出webshell,进行回答: 黑客登录系统使用的密码是_____________。得到的flag请使用NSSCTF{}格式提交。 这里我的思路是,既然要选择的时间段是黑客登录网站以后&…...
AI发展面临的问题? —— AI对创造的重新定义
一、AI的问题描述 AI与数据安全问题:随着AI技术的发展和应用,数据安全问题日益突出。AI模型训练依赖于大量数据,而这些数据中可能包含个人隐私、商业秘密等敏感信息。如果数据在采集、存储、使用过程中处理不当,可能导致数据泄露或…...
k8s学习--OpenKruise详细解释以及原地升级及全链路灰度发布方案
文章目录 OpenKruise简介OpenKruise来源OpenKruise是什么?核心组件有什么?有什么特性和优势?适用于什么场景? 什么是OpenKruise的原地升级原地升级的关键特性使用原地升级的组件原地升级的工作原理 应用环境一、OpenKruise部署1.安…...
上海亚商投顾:沪指缩量调整 PCB概念股持续爆发
上海亚商投顾前言:无惧大盘涨跌,解密龙虎榜资金,跟踪一线游资和机构资金动向,识别短期热点和强势个股。 一.市场情绪 大小指数昨日走势分化,沪指全天震荡调整,创业板指午后涨超1%。消费电子板块全天强势&a…...
QT属性系统,简单属性功能快速实现 QT属性的简单理解 属性学习如此简单 一文就能读懂QT属性 QT属性最简单的学习
4.4 属性系统 Qt 元对象系统最主要的功能是实现信号和槽机制,当然也有其他功能,就是支持属性系统。有些高级语言通过编译器的 __property 或者 [property] 等关键字实现属性系统,用于提供对成员变量的访问权限,Qt 则通过自己的元对…...
【IEEE出版丨EI检索】2024新型电力系统与电力电子国际会议(NPSPE 2024)
2024新型电力系统与电力电子国际会议(NPSPE 2024)将于8月16日至18日在中国大连举行,本届大会致力于为相关领域的专家和学者提供一个探讨行业热点问题,促进科技进步,增加科研合作的平台。本届大会涵盖新型电力系统和电力…...
【Netty】nio阻塞非阻塞Selector
阻塞VS非阻塞 阻塞 阻塞模式下,相关方法都会导致线程暂停。 ServerSocketChannel.accept() 会在没有建立连接的时候让线程暂停 SocketChannel.read()会在没有数据的时候让线程暂停。 阻塞的表现就是线程暂停了,暂停期间不会占用CPU,但线程…...
ES 操作
1、删除索引的所有记录 curl -X POST "localhost:9200/<index-name>/_delete_by_query" -H Content-Type: application/json -d {"query": {"match_all": {}} }POST /content_erp_nlp_help/_delete_by_query { "query": { &quo…...
uniapp如何实现跳转
在 UniApp 中,页面跳转主要可以通过两种方式实现:使用 <navigator> 组件和调用 UniApp 提供的导航 API。以下是这两种方式的详细说明: 1. 使用 <navigator> 组件 <navigator> 组件允许你在页面上创建一个可点击的元素&am…...
Stable-Diffusion-WebUI 常用提示词插件
SixGod提示词插件 SixGod提示词插件可以帮助用户快速生成逼真、有创意的图像。其中包含,清空正向提示词”和“清空负向提示词、提示词起手式包含人物、服饰、人物发型等各个维度的提示词、一键清除正面提示词与负面提示词、随机灵感关键词、提示词分类组合随机、动…...
单片机 PWM输入捕获【学习记录】
前言 学习是永无止境的,就算之前学过的东西再次学习一遍也能狗学习到很多东西,输入捕获很早之前就用过了,但是仅仅是照搬例程没有去进行理解。温故而知新! 定时器 定时器简介 定时器的分类 高级定时器 通用定时器 基本定时器…...
3.1、前端异步编程(超详细手写实现Promise;实现all、race、allSettled、any;async/await的使用)
前端异步编程规范 Promise介绍手写Promise(resolve,reject)手写Promise(then)Promise相关 API实现allraceallSettledany async/await和Promise的关系async/await的使用 Promise介绍 Promise是一个类,可以翻…...
3.1. 马氏链-马氏链的定义和示例
马氏链的定义和示例 马氏链的定义和示例1. 马氏链的定义2. 马氏链的示例2.1. 随机游走2.2. 分支过程2.3. Ehrenfest chain2.4. 遗传模型2.5. M/G/1 队列 马氏链的定义和示例 1. 马氏链的定义 对于可数状态空间的马氏链, 马氏性指的是给定当前状态, 其他过去的状态与未来的预测…...
红利之外的A股底仓选择:A50
内容提要 华泰证券指出,当前指数层面下行风险不大,市场再入震荡期下,可关注三条配置线索:1)A50为代表的产业巨头;2)以家电/食饮/物流/出版为代表的稳健消费龙头,3)消费电…...
wondershaper 一款限制 linux 服务器网卡级别的带宽工具
文章目录 一、关于wondershaper二、文档链接三、源码下载四、限流测试五、常见报错1. /usr/local/sbin/wondershaper: line 145: tc: command not found2. Failed to download metadata for repo ‘appstream‘: Cannot prepare internal mirrorlist: No URLs.. 一、关于wonder…...
独孤思维:盲目进群,根本赚不到钱
01 我看有些伙伴,对标同行找写作素材和灵感的时候。 喜欢把对标文章发给ai提炼总结。 这个方法好是好,但是,有一个问题。 即,无法感受全文的细节。 更无法感受作者的情感和温度。 就好像电影《记忆大师》一样。 我提取了记…...
针对indexedDB的简易封装
连接数据库 我们首先创建一个DBManager类,通过这个类new出来的对象管理一个数据库 具体关于indexedDB的相关内容可以看我的这篇博客 indexedDB class DBManager{}我们首先需要打开数据库,打开数据库需要数据库名和该数据库的版本 constructor(dbName,…...
网络编程--网络理论基础(二)
这里写目录标题 网络通信流程mac地址、ip地址arp协议交换机路由器简介子网划分网关 路由总结 为什么ip相同的主机在与同一个互联网服务通信时不冲突公网ip对于同一个路由器下的不同设备,虽然ip不冲突,但是因为都是由路由器的公网ip转发通信,接…...
Python MongoDB 基本操作
本文内容主要为使用Python 对Mongodb数据库的一些基本操作整理。 目录 安装类库 操作实例 引用类库 连接服务器 连接数据库 添加文档 添加单条 批量添加 查询文档 查询所有文档 查询部分文档 使用id查询 统计查询 排序 分页查询 更新文档 update_one方法 upd…...
Node.js 入门:
Node.js 是一个开源、跨平台的 JavaScript 运行时环境,它允许开发者在浏览器之外编写命令行工具和服务器端脚本。以下是一些关于 Node.js 的基础教程: 1. **Node.js 入门**: - 了解 Node.js 的基本概念,包括它是一个基于 Chro…...
java8 List的Stream流操作 (实用篇 三)
目录 java8 List的Stream流操作 (实用篇 三) 初始数据 1、Stream过滤: 过滤-常用方法 1.1 筛选单元素--年龄等于18 1.2 筛选单元素--年龄大于18 1.3 筛选范围--年龄大于18 and 年龄小于40 1.4 多条件筛选--年龄大于18 or 年龄小于40 and sex男 1.5 多条件筛…...
机器学习python实践——数据“相关性“的一些补充性个人思考
在上一篇“数据白化”的文章中,说到了数据“相关性”的概念,但是在统计学中,不仅存在“相关性”还存在“独立性”等等,所以,本文主要对数据“相关性”进行一些补充。当然,如果这篇文章还能入得了各位“看官…...
MySQL——触发器(trigger)基本结构
1、修改分隔符符号 delimiter $$ $$可以修改 2、创建触发器函数名称 create trigger 函数名 3、什么样在操作触发,操作哪个表 after :……之后触发 before :……之后触发 insert :……之后触发 update :……之后触…...
数字孪生定义及应用介绍
数字孪生定义及应用介绍 1 数字孪生(Digital Twin, DT)概述1.1 定义1.2 功能1.3 使用场景1.4 数字孪生三步走1.4.1 数字模型1.4.2 数字影子1.4.3 数字孪生 数字孪生地球平台Earth-2 参考 1 数字孪生(Digital Twin, DT)概述 数字孪…...
数据赋能(122)——体系:数据清洗——技术方法、主要工具
技术方法 数据清洗标准模型是将数据输入到数据清洗处理器,通过一系列步骤“清理”数据,然后以期望的格式输出清理过的数据。数据清洗从数据的准确性、完整性、一致性、惟一性、适时性、有效性几个方面来处理数据的丢失值、越界值、不一致代码、重复数据…...
【SCAU数据挖掘】数据挖掘期末总复习题库简答题及解析——中
1. 某学校对入学的新生进行性格问卷调查(没有心理学家的参与),根据学生对问题的回答,把学生的性格分成了8个类别。请说明该数据挖掘任务是属于分类任务还是聚类任务?为什么?并利用该例说明聚类分析和分类分析的异同点。 解答: (a)该数据…...
网站建设业务平均工资/如何注册一个网站
linked from http://blog.csdn.net/yjz0065/archive/2004/12/06/206147.aspx 如何对付十大时间窃贼 时间管理是事业成功的关键。 一个人、团队能否在自己的事业生涯中取得成功,秘诀就在于搞好时间管理。所以在国外,早就出现了时间管理学,…...
网站建设制作网站/企业培训视频
点击上方蓝色字体,选择“标星公众号”优质文章,第一时间送达关注公众号后台回复pay或mall获取实战项目资料视频作者:我是小茗同学来源:https://www.cnblogs.com/liuxianan1. 前言个人网站最近增加了评论功能,为了方便用…...
网站制作文章/短视频seo搜索优化
换电脑的还有一个目的就是我准备采用新的IDE了 之前一直用的是myeclipse,但是现在准备尝试idea 这边做个记录,idea的破解参考下面这个网址:http://blog.csdn.net/u013126379/article/details/52423474 idea一些简单的快捷键参考:h…...
做直播的在相亲网站交友/河南疫情最新情况
《unix环境高级编程》是stevens大师为我们留下的一本关于unix的经典著作,内容详尽深刻,对unix系统接口做了深入的描述,博主闲来无事,做了张本书的思维导图,方法很简单,不过是根据原书的目录写成,…...
易语言做网站后端/找片子有什么好的关键词
1、通过plist加载模型数据 2、controller中懒加载数据 3、设置tableView的数据源 4、写数据源的方法 5、观察演示项目,分析通过默认的cell的4种现实方式,无法实现要想要的现实效果。(自定义View) 5.1 创建xib,完成想要的cell模型,并将其命名为CZGroupBuyingCell 5.2 加载xib,…...
河北专业信息门户网站定制/aso优化榜单
一:正则表达式开发思路 首先写出正则表达式的匹配模型, 然后针对java写出java版的匹配模型.运用javaAPI Pattern 类和Matcher类来是实现java的正则匹配. 首先在匹配模式中最简单的正则表达式是不经过任何转义的,比如直接输入的英文字母s,就是代表要要匹配字母s如果要匹配两个s…...