C++ Primer 类型转换
专栏简介:本专栏主要面向C++初学者,解释C++的一些基本概念和基础语言特性,涉及C++标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级程序设计技术。希望对读者有帮助!


目录
- 4.11类型转换
- 何时发生隐式类型转换
- 算术转换
- 整型提升
- 无符号类型的运算对象
- 理解算术转换
- 其他隐式类型转换运
- 显式转换
- 命名的强制类型转换
- const_cast
- reinterpret_cast
- 旧式的强制类型转换
4.11类型转换
在C++语言中,某些类型之间有关联。如果两种类型有关联,那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。换句话说,如果两种类型可以相互转换(conversion),那么它们就是关联的。举个例子,考虑下面这条表达式,它的目的是将ival初始化为6:
int ival = 3.541 + 3;//编译器可能会警告该运算损失了精度
加法的两个运算对象类型不同:3.541的类型是double,3的类型是int。C++语言不会直接将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值。上述的类型转换是自动执行的,无须程序员的介入,有时甚至不需要程序员了解。因此,它们被称作隐式转换(implicit conversion)。
算术类型之间的隐式转换被设计得尽可能避免损失精度。很多时候,如果表达式中既有整数类型的运算对象也有浮点数类型的运算对象,整型会转换成浮点型。在上面的例子中,3转换成double类型,然后执行浮点数加法,所得结果的类型是double。
接下来就要完成初始化的任务了。在初始化过程中,因为被初始化的对象的类型无法改变,所以初始值被转换成该对象的类型。仍以这个例子说明,加法运算得到的double类型的结果转换成int类型的值,这个值被用来初始化ival。由double向int转换时忽略掉了小数部分,上面的表达式中,数值6被赋给了ival。
何时发生隐式类型转换
在下面这些情况下,编译器会自动地转换运算对象的类型:
在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型。在条件中,非布尔值转换成布尔类型。初始化过程中,初始值转换成变量的类型:在赋值语句中,右侧运算对象转换成左侧运算对象的类型。如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。函数调用时也会发生类型转换。
算术转换
算术转换(arithmetic conversion)的含义是把一种算术类型转换成另外一种算术类型。算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。例如,如果一个运算对象的类型是long double,那么不论另外一个运算对象的类型是什么都会转换成long double。还有一种更普遍的情况,当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型。
整型提升
整型提升(integral promotion)负责把小整数类型转换成较大的整数类型。对于bool、char、signed char、unsigned char、short和unsigned short等类型来说,只要它们所有可能的值都能存在int里,它们就会提升成int类型,否则,提升成unsigned int类型。就如我们所熟知的,布尔值false提升成0、true提升成1。
较大的char类型(wchar_t、char16_t、vchar32_t)提升成int、unsigned int、long、unsigned long、long long和unsigned long long中最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。
无符号类型的运算对象
如果树个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。但是如果树个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。
像往常一样,首先执行整型提升。如果结果的类型匹配,无须进行进一步的转换。如果两个(提升后的运算对象的类型要么都是带符号的、要么都是无符号的,则小类型的运算对象转换成较大的类型。
如果一个运算对象是无符号类型、另外一个运算对象是带符号类型,而一其中的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。例如,假设两个类型分别是unsigned int和int,则int类型的运算对象转换成unsigned int类型。需要注意的是,如果int型的值恰好为负值。
剩下的一种情况是带符号类型大于无符号类型,此时转换的结果依赖于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。如果不能,那么带符号类型的运算对象转换成无符号类型。例如,如果两个运算对象的类型分别是long和unsigned int,并且int和long的大小相同,则long类型的运算对象转换成unsigned int类型;如果long类型占用的空间比int更多,则unsigned int类型的运算对象转换成long类型。
理解算术转换
要想理解算术转换,办法之一就是研究大量的例子:
bool flag;char val;
short sval; unsigned short usval;
int ival; unsigned int uival;
long lval;unsigned long ulval;
float fval; double dval;
3.14159L+ 'a'; //“a“提升成int,然后该int值转换成long double
dval+ival;//ival转换成double
dval+fval;//fval转换成double
ival=dval;//dval转握成(切除小数部分后)int
flag=dval;//如果dval是0,则flag是false,否则f1ag是Lrue
cval+fval;//cval提升成int,然后该int值转换成float
sval+cval;//sval和cval都提升成int
cval+lval;//cval转援成long
ival+ulval;//ival转换成unsigned long
usval+ival;//根据unsigned short和int所占空间的大小进行提升
uival+lval;//根据hnsigned int和long所占空间的大小进行转换
在第一个加法运算中,小写字母’a’是char型的字符常量,它其实能表示一个数字值。到底这个数字值是多少完全依赖于机器上的字符集,在我们的环境中,‘a’对应的数字值是97。当把’a’ 和一个long double类型的数相加时,char类型的值首先提升成int类型,然后int类型的值再转换成long double类型。最终我们把这个转换后的值与那个字面值相加。最后的两个含有无符号类型值的表达式也比较有趣,它们的结果依赖于机器。
其他隐式类型转换运
除了算术转换之外还有几种隐式类型转换,包括如下几种。数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:
int ia[10];//含有10个整数的数组
int*ip = a;//ia转换成指向数组首元素的指针
当数组被用作 decltype 关键字的参数,或者作为取地址符(&)、sizeof及typeid等运算符的运算对象时,上述转换不会发生。同样的,如果用一个引用来初始化数组,上述转换也不会发生。指针的转换:C++还规定了几种其他的指针转换方式,包括常量整数值 0 或者字面值 nullptr 能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*。
型间还有另外一种指针转换的方式。
转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果是false;否则转换结果是true:
char*cp=get_string();
if(cp) /*...*/ //如果指针cp不是0,条件为真
while(*cp)/*...*///如果*cp不是空字符,条件为真
转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果0是一种类型,我们就能将指向0的指针或引用分别转换成指向const的指针或引用:
int i;const int &j =i; //非常量转换成const int的引用
const int*p=&i; //非常量的地址转换成const的地址
int &r = j,*q= p; //错误:不允许const转换成非常量
相反的转换并不存在,因为它试图删除掉底层const。类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。我们将看到一个例子,如果同时提出多个转换请求,这些请求将被拒绝。
我们之前的程序已经使用过类类型转换:一处是在需要标准库string类型的地方使用C风格守符串;另一处是在条件部分读入istream:
string s, t= "a value";//字符串字面值转换成string类型
while(cin >> s) // while 的条件部分把cin 转换成布尔值
条件( cin >> s )读入cin的内容并将cin作为其求值结果。条件部分本来需要一个布尔类型的值,但是这里实际检查的是istream类型的值。幸好,IO库定义了从istream向布尔值转换的规则,根据这一规则,cin自动地转换成布尔值。所得的布尔值到底是什么由输入流的状态决定,如果最后一次读入成功,转换得到的布尔值是true:相反,如果最后一次读入不成功,转换得到的布尔值是false。
显式转换
有时我们希望显式地将对象强制转换成另外一种类型。例如,如果想在下面的代码中执行浮点数除法:
int i,j;
double slope=i;
就要使用某种方法将i和/或j显式地转换成double,这种方法称作强制类型转换(cast)。
WARNING: 虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。
命名的强制类型转换
一个命名的强制类型转换具有如下形式:
cast-name<type>(expression)
其中,type是转换的目标类型而 expression 是要转换的值。如果type是引用类型,则结果是左值。cast-name是 static_cast、dynamic_cast、const_cast 和reinterpret_cast中的一种。dynamic_cast支持运行时类型识别。
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。例如,通过将一个运算对象强制转换成double类型就能使表达式执行浮点数除法:
//进行强制类型转换以便执行浮点数除法
double slope=static_cast<double>(j)/i;
当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。此时,强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息;但是当我们执行了显式的类型转换后,警告信息就会被关闭了。
static_cast对于编详器无法自动执行的类型转换也非常有用。例如,我们可以使用static_cast找回存在于void*指针中的值:
void+p=&d;//正确:任何非常量对象的地址都能存入void*//正确:将void*转接回初始的指针类型
double*dp=static_cast<double*>(p);
当我们把指针存放在void*中,并且使用static_cast将其强制转换回原来的类型时,应该确保指针的值保持不变。也就是说,强制转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义的后果。
const_cast
const_cast只能改变运算对象的底层const:
const char*pc;
char*p=const_cast<char*>(pc);//正确:但是通过p写值是未定义的行为
对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质(cast away the const)“。一旦我们去掉了树个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。然而如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。
只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用const_cast改变表达式的类型:
const char*cp;
//错误:static_cast不能转换据const性质
char*q = static_cast<char*>(cp);
static_cast<string>(cp);//正确:字符串字面值转换成string类型
const_cast<string>(cp);//错误:const_cast只改变常量属性
const_cast常常用于有函数重载的上下文中
reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。举个例子,假设有如下的转换
int *ip;
char*pc=reinterpret_cast<char*>(ip);
我们必须牢记pc所指的真实对象是一个int而非字符,如果把pc当成普通的字符指针使用就可能在运行时发生错误。例如:
string str(pc);
可能导致异常的运行时行为。
使用reinterpret_cast是非常危险的,用pc初始化str的例子很好地证明了这一点。其中的关键问题是类型改变了,但编译器没有给出任何警告或者错误的提示信息。当我们用一个int的地址初始化pc时,由于显式地声称这种转换合法,所以编译器不会发出任何警告或错误信息。接下来再使用pc时就会认定它的值是char*类型,编译器没法知道它实际存放的是指向int的指针。最终的结果就是,在上面的例子中虽然用pc初始化str没什么实际意义,甚至还可能引发更糟糕的后果,但仅从语法上而言这种操作无可指摘。查找这类问题的原因非常困难,如果将ip强制转换成pc的语句和用pc初始化string对象的语句分属不同文件就更是如此。
WRNING: reinterpret_cast本质上依赖于机器.要想安全地使用reinterpret_cast必须对涉及的类型和编译嚣实现转换的过程都非常了解。
建议:避免强制类型转换
强制类型转换干扰了正常的类型检查,因此我们强烈建议程序员避免使用强制类型转换。这个建议对于reinterpret_cast尤其适用,因为此类类型转换总是充满了风险。在有重载函数的上下文中使用const_cast无可厚非,关于这一点将在;但是在其他情况下使用const_cast也就意味着程序存在某种设计缺陷。其他强制类型转换,比如static_cast和dynamic_cast,都不应该频素使用。每次书写了一条强制类型转换语句,都应该反复斟酌能否以其他方式实现相同的目标。就算实在无法避免,也应该尽量限制类型转换值的作用域,并且记录对相关类型的所有假定,这样可以减少错误发生的机会。′
旧式的强制类型转换
在早期版本的C++语言中,显式地进行强制类型转换包含两种形式:
type(expr);//函数形式的强制类型转换
(type)expr;//C语言风格的强制类型转换
根据所涉及的类型不同,旧式的强制类型转换分别具有与const_cast、static_cast或reinterpret_cast相似的行为。当我们在某处执行旧式的强制类型转换时,如果换成const_cast和static_cast也合法,则其行为与对应的命名转换一致。如果替换后不合法,则旧式强制类型转换执行与reinterpret_cast类似的功能:
char *pc=(char*)ip;//ip是指向整数的指针
的效果与使用retnterpret_cast一样。
相关文章:
C++ Primer 类型转换
欢迎阅读我的 【CPrimer】专栏 专栏简介:本专栏主要面向C初学者,解释C的一些基本概念和基础语言特性,涉及C标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级…...
【CS61A 2024秋】Python入门课,全过程记录P7(Week13 Macros至完结)【完结撒花!】
文章目录 关于新的问题更好的解决方案Week13Mon Macros阅读材料Lab 11: Programs as Data, MacrosQ1: WWSD: QuasiquoteQ2: If ProgramQ3: Exponential PowersQ4: Repeat Wed SQL阅读材料Disc 11: MacrosQ1: Mystery MacroQ2: Multiple AssignmentQ3: Switch Optional Contest:…...
SSH隧道+Nginx:绿色通道详解(SSH Tunnel+nginx: Green Channel Detailed Explanation)
SSH隧道Nginx:内网资源访问的绿色通道 问题背景 模拟生产环境,使用两层Nginx做反向代理,请求公网IP来访问内网服务器的网站。通过ssh隧道反向代理来实现,重点分析一下nginx反代的基础配置。 实验环境 1、启动内网服务器的tomca…...
LabVIEW用户界面设计原则
在LabVIEW开发中,用户界面(UI)设计不仅仅是为了美观,它直接关系到用户的操作效率和体验。一个直观、简洁、易于使用的界面能够大大提升软件的可用性,尤其是在复杂的实验或工业应用中。设计良好的UI能够减少操作错误&am…...
Datawhale 数学建模导论二 2025年2月
第6章 数据处理与拟合模型 本章主要涉及到的知识点有: 数据与大数据Python数据预处理常见的统计分析模型随机过程与随机模拟数据可视化 本章内容涉及到基础的概率论与数理统计理论,如果对这部分内容不熟悉,可以参考相关概率论与数理统计的…...
SQL CASE表达式的用法
SQL CASE表达式的用法 一、CASE表达式的基础语法简单CASE表达式搜索CASE表达式 二、简单CASE表达式的应用示例三、搜索CASE表达式的应用示例四、CASE表达式在聚合函数中的应用五、嵌套CASE表达式的应用 今天在也无力用到了CASE表达式,于是有了这篇博客,C…...
趣味魔法项目 LinuxPDF —— 在 PDF 中启动一个 Linux 操作系统
最近,一位开源爱好者开发了一个LinuxPDF 项目(ading2210/linuxpdf: Linux running inside a PDF file via a RISC-V emulator),它的核心功能是在一个 PDF 文件中启动并运行 Linux 操作系统。它通过巧妙地使用 PDF 文件格式中的 Ja…...
win32汇编环境,窗口程序使用跟踪条(滑块)控件示例一
;运行效果 ;win32汇编环境,窗口程序使用跟踪条(滑块)控件示例一 ;生成2条横的跟踪条,分别设置不同的数值范围,设置不同的进度副度的例子 ;直接抄进RadAsm可编译运行。重要部分加备注。 ;下面为asm文件 ;>>>>>>>>>>>>>>>>>…...
mars3d接入到uniapp的时候ios上所有地图的瓦片都无法加载解决方案
用的是【Mars3d】官网的uniapp的仓库,安卓没有问题,但是ios的不行 相关链接 mars3d-uni-app: uni-app技术栈下的Mars3D项目模板 解决方案:感觉所有图片请求全被拦截了 uniapp的ios内核不允许跨域,需要先把瓦片下载后转base64&…...
使用 Notepad++ 编辑显示 MarkDown
Notepad 是一款免费的开源文本编辑器,专为 Windows 用户设计。它是替代记事本(Notepad)的最佳选择之一,因为它功能强大且轻量级。Notepad 支持多种编程语言和文件格式,并可以通过插件扩展其功能。 Notepad 是一款功能…...
wordpress主题制作
工具/原料 <P><BR>使用divcss语言编写的html静态页面一个</P> <P>Macromedia Dreamweaver软件<BR></P> WordPress主题结构分析 1 1、index.php首页模板(最基本) ---- 1、header.php头部 ---- 2、sidebar.php侧边…...
MybatisPlus常用增删改查
记录下MybatisPlus的简单的增删改查 接口概述 Service和Mapper区别 Mapper简化了单表的sql操作步骤(CRUD),而Serivce则是对Mapper的功能增强。 Service虽然加入了数据库的操作,但还是以业务功能为主,而更加复杂的SQL…...
Citus的TPCC、TPCH性能测试
Citus的TPCC、TPCH性能测试 文章目录 Citus的TPCC、TPCH性能测试测试的目的适用范围测试环境架构信息硬件配置操作系统软件版本 测试结果TPCC测试测试结果TPCH测试测试结果 一、环境部署1.1、安装BenchmarkSQL1.2、PostgreSQL安装1.3、nmon部署1.4、TPC-H测试的生成数据工具安装…...
蓝桥杯---颜色分类(leetcode第75题)题解
文章目录 1.问题重述2.思路分析3.代码分析 1.问题重述 颜色分类,实际上就是赋予了三种颜色不同的数值,0,1,2分别代表的就是一个类型的颜色,我们题目说的是对于颜色进行分类,实际上就是对于0,1,2进行分类,我们把很多数…...
C语言基础13:循环结构 for和while
循环结构 什么是循环结构 代码在满足某种条件的前提下,重复执行,就叫做循环结构。 循环的分类 无限循环:其实就是死循环,程序设计中尽量避免无限循环,如果非要使用,那么这个循环一定要在可控范围内。有…...
六西格玛设计培训如何破解风电设备制造质量与成本困局
2023年,中国风电行业装机容量突破4.3亿千瓦,稳居全球第一,但高速扩张背后暗藏隐忧: 质量痛点:叶片开裂、齿轮箱故障等缺陷频发,运维成本占项目全生命周期成本超30%;成本压力:原材料…...
【Android开发】安卓手机APP使用机器学习进行QR二维码识别
前言:本项目是一个 Android 平台的二维码扫描应用,具备二维码扫描和信息展示功能。借助 AndroidX CameraX 库实现相机的预览、图像捕获与分析,使用 Google ML Kit 进行二维码识别。为方便大家了解项目全貌,以下将介绍项目核心代码文件 MainActivity.java 和 AndroidManifes…...
Zabbix-监控SSL证书有效期
背景 项目需要,需要监控所有的SSL证书的有效期,因此需要自定义一个监控项 实现 创建自定义脚本 在Zabbix的scripts目录(/etc/zabbix/scripts/)下创建一个新的shell脚本check_ssl.sh,内容如下 #!/bin/bash time$(echo | openssl s_client…...
生成式聊天机器人 -- 基于Pytorch + Global Attention + 双向 GRU 实现的SeqToSeq模型 -- 上
生成式聊天机器人 -- 基于Pytorch Global Attention 双向 GRU 实现的SeqToSeq模型 -- 上 前言数据预处理下载并加载数据原始数据格式化数据清洗与字典映射转换为模型需要的数据格式 SeqToSeq 模型Encoder 编码器Decoder 解码器全局注意力机制解码器实现 前言 本文会介绍使用…...
Kickstart自动化安装过程中自动选择较小的磁盘安装操作系统
Kickstart自动化安装过程中自动选择较小的磁盘安装操作系统 需求 在实际生成操作过程中,一般会遇到物理服务器存在多块盘的情况。 安装过程中,磁盘的标签是随机分配的,并不是空间较小的盘,就会使用较小的磁盘标签 而需求往往需要…...
Android Wi-Fi 连接失败日志分析
1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分: 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析: CTR…...
Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)
概述 在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过,在涉及到多个子类派生于基类进行多态模拟的场景下,…...
Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...
【大模型RAG】Docker 一键部署 Milvus 完整攻略
本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装;只需暴露 19530(gRPC)与 9091(HTTP/WebUI)两个端口,即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...
【无标题】路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论
路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论 一、传统路径模型的根本缺陷 在经典正方形路径问题中(图1): mermaid graph LR A((A)) --- B((B)) B --- C((C)) C --- D((D)) D --- A A -.- C[无直接路径] B -…...
