2. 核心细节解析与实操要点
讲完了设计层面的思路,下面进入 exexplicit 的关键机制与语法细节。这一节我会尽量把 C++ 标准的措辞"翻译"成人话,结合代码样例,把每一处容易踩坑的地方都摊开讲清楚。你如果能把这一节的内容真正吃透,写代码的时候基本就不会再被隐式转换"背刺"了。
2.1 explicit 到底做了什么:禁止拷贝形式初始化
explicit 关键字的语法非常简单,就是在构造函数声明前加上这个修饰词:
class Widget { public: explicit Widget(int value); // 禁止隐式创建 // ... };但这个关键字生效的规则,远比初学者想象的更微妙。C++ 标准里,explicit 真正禁止的是"拷贝形式初始化",也就是使用=语法的初始化方式:
Widget w = 42; // 编译错误(因为构造函数是 explicit) Widget w2(42); // 可以,这是直接初始化 Widget w3{42}; // 可以,这是列表初始化 Widget w4 = {42}; // C++11 之前不行,C++11 之后也不行(列表拷贝形式同样被禁止)这里面的"为什么"很关键:直接初始化(括号或花括号)是你明确地在构造对象,编译器知道你心里想的是什么;而拷贝形式初始化带了一个=,语义上暗示着"先把 42 转换成一个 Widget,再把它拷贝给 w"。explicit 拦截的正是这种"先转换再拷贝"的隐式路径。
有个非常经典的类比:直接初始化就像你明明说了一句"给 我 一 杯 咖 啡",拷贝初始化则像"我 = 咖啡",然后系统自主判断你想要的是一杯咖啡而不是一袋咖啡豆。explicit 就是那个在系统里写死的规则——不许替我猜,我只接受明确说出口的请求。
2.2 哪些构造函数需要加 explicit:三种典型场景
不是所有构造函数都"应该"加 explicit,但有三类场景强烈建议必须加:
第一类:单参数构造函数。这是最常见、也最危险的情况。只要构造函数只有一个参数,它天然就是一条隐式转换通道。
class Score { public: Score(int s) : value(s) {} private: int value; }; void printScore(const Score& s); // 一个接收 Score 的函数 printScore(95); // 编译器会默默调用 Score(95) 把你"骗"进去加了 explicit 之后,printScore(95)就无法编译,你必须显式写出printScore(Score(95))或printScore(static_cast<Score>(95))。这多写出来的几个字符,就是你对代码可读性交的"税",也是随手关上的"潘多拉魔盒"。
第二类:多参数构造函数但除第一个外都有默认值。这个场景非常阴,因为代码看起来"人畜无害":
class Timer { public: Timer(int duration, int unit = 1) : // 第二个参数有默认值 m_duration(duration * unit) {} private: int m_duration; }; void schedule(const Timer& t); schedule(30); // 编译器会调用 Timer(30, 1) —— 完全隐式!这类构造函数本质上是"可变参数个数"的构造函数,只要调用时只需要一个实参,就会退化成隐式转换通道。遇到这种情况,同样需要 explicit 来堵死这条暗路。从代码审查的角度看,凡是不希望被隐式调用的"有一个参数就能构造"的构造函数,都应该加 explicit。
第三类:转换运算符。C++11 之后,explicit 被允许用在转换运算符上:
class Status { public: explicit operator bool() const { return m_code == 0; } private: int m_code; };这个我在后面的 3.2 节会重点展开,这里先记住结论:加了 explicit 的operator bool只能出现在明确的布尔上下文中(如if条件、while循环、三目运算符),不会在算术表达式或位运算里"偷偷"帮你转换。
2.3 加了 explicit 不等于禁止一切转换:static_cast 与函数式转换
有一个新手很容易产生误解的点:explicit 并不是让这个构造函数"退隐江湖",它只是堵住了"隐式调用"的那条路。显式的类型转换仍然完全合法:
Widget w1 = 42; // 错误:隐式转换被禁止 Widget w2 = static_cast<Widget>(42); // 正确:显式转换 Widget w3 = Widget(42); // 正确:函数式转换这就好比你把家里的备用钥匙收起来了,但正门钥匙你随时可以用。explicit 语义的本质是"你是不是明确表达意图"——显式转换相当于你举着身份证对门口保安说"就是我";隐式转换则是你直接往里走,保安还以为是工作人员没拦你。
我在实际项目中就遇到过因为这种混淆导致的代码风格问题。有同事给构造函数加了 explicit 之后,硬生生把所有Widget w = 42;改成了Widget w = Widget(42);,虽然能编译能运行,但这样用完全失去了 explicit 的意义——你写的还是"拷贝形式初始化",只是右边多套了一层。更规范的做法是用括号或花括号直接初始化:Widget w(42);或Widget w{42};。
2.4 explicit 与"= default"、移动构造、初始化的交互细节
这里有几个比较冷门但面试常考的细节,我梳理成三个要点:
第一个:explicit 不能用于默认构造函数(无参构造函数)。因为无参构造不涉及任何参数,没有"转换"这回事。C++ 标准明确禁止explicit Widget();这种写法。如果复制控制成员声明为explicit,也是非法的。这背后逻辑很简单:explicit 管的是"参数到对象的映射",没有参数自然就没有映射。
第二个:explicit 与= default可以共存。你可以写出explicit Widget(int) = default;这种语法,不冲突。前者声明意图,后者声明实现方式。这在一些需要保留构造语义又不想手写实现体的场景下很常见。
第三个:拷贝构造和移动构造通常不应该加 explicit。拷贝构造天然就是"用对象构造对象",如果加了 explicit,标准库里大量按值传参、返回对象的代码都会无法编译,std::vector<Widget> v; v.push_back(w);也会直接跪。这个"区分"原则要记住:explicit 加在"从一个非同类对象构造"的路径上,而不是"从同类对象复制/移动"的路径上。这个边界是 C++ 设计者划定的,跟随这个边界走,代码一般不会出大问题。
3. 实操过程与核心环节实现
我在自己的项目里,把 explicit 从"知道"变成"理解",靠的是一系列能动手复现的实验。这一节我不讲空话,把我在 VS Code + MinGW 环境下的完整实操过程写出来,每一步都配有代码、编译指令和观察到的现象。你跟着做一遍,对 explicit 的感受绝对比看上十篇文章更深刻。
3.1 环境准备:VS Code 里跑通第一个 explicit 实验
我的日常开发环境其实很朴素,CLion 和 VS Code 都用,但为了验证 explicit 的编译行为,VS Code + MinGW 反而是最快的。如果你还没有配好环境,按下面的流程操作,10 分钟能跑起来:
- 下载 MinGW-w64(建议选 x86_64-win32-seh 版本,别选 thread 或者 dwarf 的旧版,兼容性更好)。
- 把 MinGW 的
bin目录加入系统 PATH。 - 在 VS Code 里装 C/C++ 扩展,是 Microsoft 官方出的那个。
- 新建
explicit_test.cpp文件,写完代码后,在终端用g++ explicit_test.cpp -o test编译,再运行生成的可执行文件。
我这里有一个最简洁的测试代码,你可以直接复制试试:
// explicit_test.cpp #include <iostream> class Secret { public: explicit Secret(int code) : m_code(code) {} int code() const { return m_code; } private: int m_code; }; void reveal(const Secret& s) { std::cout << "Secret code: " << s.code() << std::endl; } int main() { // reveal(42); // 取消注释会发现编译失败 reveal(Secret(42)); // 正确 Secret s = static_cast<Secret>(42); // 正确 // Secret s2 = 42; // 取消注释会发现编译失败 std::cout << "All explicit calls passed." << std::endl; return 0; }把reveal(42);和Secret s2 = 42;的行注释去掉,保存后再编译,你会看到 g++ 给出明确的错误信息,大意是"无法从 int 转换为 Secret"或"没有可行的转换函数"。这个报错就是 explicit 生效的直接证据。
我在初学的时候有个误区,以为编译错误信息会直接说"explicit 禁止了转换"。实际上 g++ 的报错不会这么友好,它只会告诉你找不到合适的构造函数或转换。这也是为什么很多人在大型项目里排查这类编译错误时,要花非常久的时间——报错不是那么直给。这个点我会在 4.2 节专门讲怎么快速识别这类问题。
3.2 一个让人印象深刻的对比实验:隐式 vs 显式构造
下面这个实验是我在给团队做内部分享时反复用的,效果很好。我用一个自定义的字符串类,模拟了隐式转换导致程序行为"静默出错"的场景:
class MyString { public: // 故意不加 explicit,复现隐患 MyString(const char* str) : m_str(str) {} const char* c_str() const { return m_str; } private: const char* m_str; }; class Logger { public: void log(const MyString& s) { std::cout << "[LOG] " << s.c_str() << std::endl; } void log(bool enabled) { std::cout << "[FLAG] " << (enabled ? "on" : "off") << std::endl; } }; int main() { Logger logger; logger.log("hello world"); // 两个 log 重载都能接受?编译器选了哪个? return 0; }这段代码的问题很有迷惑性:"hello world"的类型是const char*,它可以直接用MyString(const char*)构造出一个临时对象,导致log(const MyString&)被选中;但"hello world"是字符串字面量,它不能隐式转成bool,所以log(bool)不会被选。看起来结果"天经地义"。但如果我们在main里把字符串换成一个整数:
logger.log(1); // 1 不能隐式转成 MyString,但它能隐式转成 bool!这时候编译器会毫不犹豫地选择log(bool),因为int -> bool是标准转换。这种"你以为是记日志,实际只是打了个标记"的问题,在真实项目里排查起来极其痛苦。如果你给MyString的构造函数加上 explicit,log(1)会在编译期直接报错,把你从"静默的坑"里拉出来。
这个对比是我最建议你亲手跑一遍的。它不需要多复杂的框架,但能把隐式转换的危害从"理论"变成"切身感受"。这就是为什么我一直强调,explicit 不是代码洁癖,它是让你在出错的第一时间就收到通知的机制。
3.3 现代写法:用 C++11 的列表初始化来强化 explicit
C++11 引入了花括号初始化(列表初始化),它天然比圆括号更严格。这里有个很实用的技巧:explicit配合花括号初始化,可以让代码的意图更清晰。我在代码规范里通常推荐这样组合使用:
class Config { public: explicit Config(int version, int level = 0) : m_version(version), m_level(level) {} private: int m_version; int m_level; }; Config c1(2, 3); // 直接初始化,explicit 不受影响 Config c2{2, 3}; // 列表初始化,explicit 不受影响 Config c3 = {2, 3}; // 错误,即使是 C++11 也不允许 Config c4 = Config{2, 3}; // 正确,显式转换Config c3 = {2, 3};在 C++11 之前是合法的,在 C++11 之后被禁止了,这个变化很多人没意识到。原因是 C++11 对"列表初始化 + 拷贝初始化"的语义做了收紧——只要构造函数是 explicit,花括号的拷贝形式初始化同样被禁止。这让代码在"should it be allowed?"这个问题上变得更加保守,对代码安全是有利的。
这里有一个与现代 C++ 社区审美一致的点:尽量用花括号初始化,因为它能规避很多烦人的"most vexing parse"问题。比如Config c();在 C++ 里会被解析成函数声明而不是对象创建,用Config c{};就不会有这种歧义。explicit 加上花括号初始化,基本上能把你构造对象时的意外路径封死一大半。
3.4 实际项目里的"最小改动"修法:从隐式到显式的迁移技巧
说了这么多,最后落到一个真实场景:如果团队代码库里有一批没加 explicit 的构造函数,现在想逐步消除隐患,该怎么动刀?
我整理出一套我实际用过的迁移方案,按照"影响面从小到大"排列:
先修转换运算符。
operator bool这一类不加 explicit 的危害最大,因为布尔上下文无处不在(if、while、!、三目运算、逻辑表达式)。先给这类运算符加 explicit,对代码的编译影响通常最小,收益却最大。再修单参数构造函数。逐个类检查,遇到单参数构造函数先加 explicit,然后编译一次,把报错的位置一个个人工确认,看是不是"本来就不该隐式转换"的调用点。实践中大约 70% 的报错点都是无意识的隐式转换,改成显式构造后代码反而更清晰。
最后处理带默认参数的"伪单参数"构造函数。这类最隐蔽,也最容易在编译通过的情况下被忽略。建议写一个简单的正则或 AST 扫描脚本,找出所有"参数个数 >= 1 且后续参数都有默认值"的构造函数,逐个评估。
迁移过程中有一个非常实用的工具思维:先用-fpermissive编译选项让项目在"宽松模式"下通过编译,记录所有 warning 点位,再逐个手动修复。这个选项不是用来上生产的,但用来量化"有多少隐式转换调用点"非常好用。我在一个中型项目上跑过一次,整整发现了 57 处隐式转换调用,其中一半以上都是bool和整数之间的混淆,修复后编译错误数量反而更少了,因为一些以前被隐式转换掩盖的歧义提前暴露了出来。
4. 常见问题与排查技巧实录
这一节的价值,是我在过去几年里踩坑、填坑、给别人review代码的真实记录。我把它整理成问题速查、排查流程和面经问答三个角度,希望能帮你少走点弯路。
4.1 编译报错了:这锅是 explicit 的吗?
症状 1:"不存在从 X 到 Y 的适当构造函数"或"无法转换"
这种报错最常见的原因,就是你的目标类型构造函数是 explicit 的,而你试图做隐式转换。排查流程三步走:
- 看报错行,确认是不是"= 赋值 + 右侧是一个不同类型的值"。
- 翻到目标类的构造函数定义处,看有没有 explicit。
- 如果是 explicit,把隐式赋值改成显式构造:
Y y(x);或Y y = static_cast<Y>(x);。
症状 2:函数重载的时候意外调用了错误版本
这是隐式转换的另一大坑。一个函数有多个重载版本,参数分别是bool、int、string,你传入一个char*,编译器可能先走bool而不是string,因为char* -> bool是标准转换,而char* -> string需要调用用户定义的构造函数。这种问题表现为"代码能跑,但行为不对"。排查手段是给所有单参数构造函数加 explicit,编译一遍,让所有隐式路径暴出来。
症状 3:if (obj)编译失败
C++11 之前,operator bool不加 explicit 是很常见的写法,但 C++11 之后建议写成explicit operator bool() const。加了 explicit 的operator bool仍然可以在if (obj)里正常工作,因为if条件属于标准要求的"布尔上下文",explicit 允许在这里发生。但如果你写bool b = obj;或int x = obj + 1;,就会报错。很多人一脸懵,其实这只是你还没适应 explicit 转换运算符的新规则。
我把这些常见场景整理成一个速查表:
| 代码写法 | explicit 构造函数 | 非 explicit 构造函数 | 含义 |
|---|---|---|---|
Y y = x; | 编译错误 | 可能隐式构造 | 拷贝形式初始化 |
Y y(x); | 可用 | 可用 | 直接初始化 |
Y y{x}; | 可用 | 可用 | 列表初始化(推荐) |
f(x)其中 f 接收 Y | 编译错误 | 可能隐式构造 | 实参隐式转换 |
static_cast<Y>(x) | 可用 | 可用 | 显式转换 |
bool b = obj;(operator bool 非 explicit) | N/A | 可用 | 可能导致误用 |
if (obj)(operator bool 为 explicit) | N/A | 可用 | 标准布尔上下文 |
这张表建议你直接存下来,写代码前瞄一眼,能挡掉 90% 的隐晦编译错误。
4.2 借助编译器的力量:用静态检查工具批量找出漏网之鱼
手动给每个构造函数加 explicit 是不现实的,尤其是大型项目。我的做法是用 clang-tidy 的modernize-use-explicit检查项做批量扫描。这条检查规则会自动标记所有"应该加 explicit 但没有加"的构造函数,并给出修改建议。
具体操作如下:
clang-tidy -checks='modernize-use-explicit' your_source.cpp -- -std=c++17它会把你代码里所有单参数构造函数(或带默认参数的多参数构造函数)标记出来。输出的警告里会提示你"把这个构造函数声明为 explicit"。
我在一个 20 万行的老项目里跑过一次,结果让我很震惊:有 80 多处构造函数需要加 explicit,其中有十几处是operator bool,另外二十多处是一个资源管理类的隐式转换入口。花了整整一下午把这些全部修完,编译和测试通过后,后续半年里因隐式转换导致的隐性 bug 几乎绝迹。这是个投入产出比很高的工程实践。
补充一个技巧:如果你的团队用的是 Visual Studio,可以打开/permissive-编译选项,它能让编译器在一个更严格的标准兼容模式下工作,对隐式转换等问题的诊断更严格。这是微软官方在 VS2019 16.8 之后默认推荐的行为,老代码如果直接打开这个选项可能产生一堆编译错误,但这些错误多半都是潜在的设计问题,值得一个个看。
4.3 面试高频考点:explicit 背后的"意图表达"哲学
作为一个经常参与 C++ 技术面试的人,我可以说 explicit 是面试官非常爱问的一个点,但很多候选人回答得都很浅。最基础的问题是"explicit 是干嘛的",进阶一点会问"为什么需要它",再深一层会问"C++11 之后它有什么新用法"。
我这里给出一个我比较认可的"高分段回答框架":
- 基础层:explicit 修饰构造函数,防止隐式类型转换。
- 进阶层:隐式类型转换虽然方便,但会掩盖代码意图,导致函数重载匹配错误、资源管理混乱、语义被无声改变。explicit 让代码"得 按 规 矩 办 事"。
- 深层:explicit 是对"哪些转换是合理的"这件事的静态声明。程序员通过它告诉编译器与读者,这条转换路径必须显式触发,不允许隐式发生。它呼应了 Python 之禅里的那句"显式优于隐式"。
- 应用层:现代 C++ 中,explicit 不仅可以修饰构造函数,还可以修饰转换运算符(C++11 之后)。实践规范是"几乎所有的单参数构造函数都应该加 explicit",除非确实需要隐式转换(比如实现一个数值兼容的包装类型)。
最后一层尤其重要。很多候选人能答到第三层,但落实到工程实践上仍然含糊。实际上,真正能体现水平的是你说出这样一句:"我会默认给单参数构造函数加 explicit,然后在使用到隐式转换的极少数场景下再显式去掉它。"这才是资深开发者的思维模式——先防守,后开窗,而不是先开放,再补救。
5. 我的使用规范与扩展思考
前面把 explicit 的技术细节讲得比较透了,这一节我想以一个一线开发者的身份,聊聊我在代码规范、代码评审和长期维护中对 explicit 的态度,以及它和现代 C++ 其他特性之间的联系。这些内容不算"标准答案",但都是我个人觉得非常值得参考的实操经验。
5.1 代码评审中对 explicit 的"默认规则"
我们团队在代码评审时有一条铁律:凡是单参数构造函数(包括带默认参数的多参数构造函数),默认都应该加 explicit,除非有充分理由不加,且有注释说明为什么。这条规则几乎不用讨论,因为它的收益太明确了。
有同事问过:"那我不想加,因为我在很多地方都依赖隐式转换,加了 explicit 会编译失败。"我的回答是:"这恰恰说明你的代码高度耦合了隐式转换路径,你应该重新设计这些调用点,让它们的意图更明确。如果实在没法改,再考虑放开 explicit,但要在注释里写明'这里依赖隐式转换是为了 XXX 设计考虑'。"
这种做法背后有一个理念:explicit 不只是一个语法特性,它是代码自我描述的载体。读代码的人看到一个构造函数是 explicit 的,就能立刻推断出"这个类不希望被别人随便转换过来",这比读注释要直接得多。反过来,如果一个构造函数不是 explicit 的,读者就会想"为什么这里故意不做防护",这促使人去理解设计意图。
在评审中我还会检查一件事:explicit 是否被用在了错误的地方。比如有人会把explicit加到拷贝构造函数或移动构造函数上,这种用法会让标准容器、算法的使用变得极其别扭,几乎可以肯定是个错误的设计决策。遇到这种情况,我会建议把它去掉,然后重新审视调用点是不是有其他问题。explicit 的"度"很重要,它不是万能的,也不是用得越多越好。
5.2 什么时候真的不该加 explicit:少数需要隐式转换的场景
说了这么多 explicit 的好处,也得聊一聊"反例"。在我目前的经验里,有以下三类场景,不加 explicit 是合理的:
第一类:自定义数值类型。比如你写了一个FixedPoint(定点数)类,它从int或double构造是"语义自然"的。用户写出FixedPoint f = 3;时,心里想的就是"3 这个数值变成定点数",这种转换是符合直觉的。加了 explicit 反而会把这些自然表达变成一坨强制转换,纯属添堵。
第二类:作用等同于"适配器"的包装类型。例如std::string_view从std::string构造、std::function从函数对象构造,它们的设计目标就是无缝衔接已有类型。这种场景下,隐式转换是特性不是 bug。
第三类:需要模拟内建类型语义的 RAII 包装器。比如一个BoolExpression类,你希望它能在if里直接判断真假,又不想让它在算术运算中出现,C++11 之后用explicit operator bool恰好能做到。这里要强调的是,即使是operator bool,也建议加 explicit,因为它能自动在布尔上下文生效,同时避免误入算术表达式。
区分"该加"和"不该加"的原则就一句话:如果隐式转换能让代码更自然、更安全,就保留;如果隐式转换只是"少写了几个字符",却模糊了意图,就加 explicit。用这个标准去审视,大多数争议都能很快平息。
5.3 explicit 与 C++17/20 的联动:概念、推导与契约
explicit 并不仅仅是一个孤立的关键字,它在现代 C++ 里和很多东西产生了联动。我简单讲两点我在实践中体会比较深的。
第一点:explicit 与类模板参数推导(CTAD)。写模板时,你可能需要提供"推导指引"(deduction guide)。explicit 在推导指引中也有作用。看这个例子:
template <typename T> class Wrapper { public: explicit Wrapper(T val) : m_value(val) {} private: T m_value; }; Wrapper w(42); // C++17 推导为 Wrapper<int>,OK Wrapper w2 = 42; // 错误:explicit 生效,禁止隐式转换explicit 在这里的语义仍然是"禁止拷贝形式初始化",但它和模板推导结合后,会产生一些看起来有点违反直觉的行为。特别是在写泛型代码时,一个构造函数是不是 explicit,会直接影响std::is_convertible_v和std::is_constructible_v这两个类型萃取的结果。这也是 C++ 标准库实现里大量使用explicit来精确控制类型转换的底层原因。
第二点:explicit 与 Concepts(C++20)。C++20 的概念(requires 表达式)和 explicit 结合后,可以写出更精确的"接口契约"。比如你可以定义一个概念,要求一个类型可以从另一个类型显式构造但不能隐式转换:
template <typename From, typename To> concept explicitly_convertible_to = requires(From f) { static_cast<To>(f); };这种用法让 explicit 从"类内部的声明"演进为"接口之间关系的描述",在泛型设计中非常有价值。我自己在实现一些库组件时,会明确区分"可隐式转换"和"可显式转换"两组概念,让调用方的约束更精确。这种精细的约束控制,正是现代 C++ 区别于"能跑就行"的老派写法的标志之一。
5.4 从 explicit 出发:如何把 C++ 的"规则感"用到极致
最后分享一个我长期以来的建议:学习 C++ 关键字不要孤立地背语法,要把它们放在"语言的约束体系"里去理解。explicit 只是约束体系中的一个控制点,类似的控制点还有delete、default、noexcept、const等。每个关键字的设计初衷,都是在给程序员提供一种"用代码表达意图"的方式。
举个例子,= delete可以禁止拷贝、禁止某种类型的参数;noexcept声明函数不会抛异常;const声明数据不可变。它们和 explicit 一样,都在做同一件事——把设计者的意图转化为编译器可执行的规则。一旦这些规则定下来,错误就会被提前拦截,代码的意义也会更清晰。
我经常在团队内部鼓励大家思考一个问题:"如果我写的代码,别人六个月后回来看,能一眼看出哪些地方是设计底线,哪些地方是临时妥协吗?" explicit 就是这种"设计底线"的标记。它告诉后来的维护者:"这个类型不允许隐式转换,这是设计上不可动摇的决定,别在重构的时候悄悄把它去掉。"
我在实际项目中见过太多次"因为一个隐式转换的 bug,debug 了整整一天"的案例。而解决问题的手段,往往只是给构造函数加上一个 explicit。这就是这个小关键字的巨大价值。它看起来不起眼,却是保证 C++ 代码可维护性的一颗重要压舱石。
如果你正在学 C++,或者已经在写 C++,我建议你现在就做一件事:把你手头所有单参数构造函数检查一遍,该加 explicit 的加上,然后重新编译一次。你可能会惊讶地发现,编译器会帮你暴露出许多"你以为在正确工作、其实早就走错了路"的代码路径。这个过程,就是 C++ 对你代码质量的一次免费体检。
我在实际开发中最深的体会就是:C++ 是一门愿意"放权"给你、也愿意"约束"你的语言。explicit 是这种双重性格的缩影——它把克制和自由同时交到你手里,至于怎么选,取决于你对自己代码意图的重视程度。