人工填这些数据标注累吗TM太累了,有其他方案实现数据标注累吗自动录入的吗

鉴于题主对人工智能这方面的知識不太懂所以接下来小编会从病案质控存在什么问题、怎么用人工智能去解决病案质控这两方面进行回答。

一、病案质控消耗了大量的囚才资源然而矛盾并没有得到有效解决

质控人员一般会从八个方面去质控一份病历,分别是:病案首页入院记录,病程记录围手术期记录,出院记录知情同意书,医嘱及辅助检查病历书写基本原则。以病案首页为例前,病案主要信息的检索仍然是以首页数据标紸累吗库的内容为基础进行的根据首页检索的信息,我们才可与各种资源进行连接所以,病案首页信息是打开病案信息库的一把“万能钥匙”它的内容包括了病人基本情况、住院诊断、手术及操作情况、其他信息与住院医疗费用分类情况。质控人员会针对这些内容进荇详细的检查例如入院诊断和出院诊断是否书写错误或者漏填,药物过敏是否空白或者书写错误是否有详细的通讯方式和地址等等。

鉯上这张图仅仅是一份病历的病案首页质控人员要对以上这么多内容进行人为的核实,而要涉及到一份完整的病案质控的内容也就更哆了。

这张表包含了全面的评分细则可以为质检人员的病案质控工作提供具体可操作的评定依据,有助于医院评审及各类检查中病历书寫质量的评估、病历环节质量的评审以及终末质量检查等至此,病案质控的管理问题似乎已经得到了完美的解决然而实际的情况却并非如此。

在实践中海量而复杂的病案质控工作与有限的人才资源之间产生了巨大的矛盾。

1、海量的质控工作和有限的人力精力之间产生叻矛盾病案质控工作单调枯燥,质检人员每天需要审阅大量病案囊括的内容极为丰富和复杂,质控人员需要逐项审阅工作强度高,需要耗费极大的精力和时间

2、高素质要求的质控内容与紧缺的质检人才之间产生了矛盾。病案质控需要全科医学能力和丰富的临床经验对质控人员的医学水平要求极高,面对紧缺的医疗人才临床科室的人才需求尚不能满足,质控人员的招聘就更加困难了

3、紧迫的质控需求和低覆盖率的质控范围之间产生了矛盾。随着医疗纠纷案件的不断增加病案质控的需求也变得非常紧迫。而质检人员无法消化那麼多基数庞大且不断累加的病案造成病案质控覆盖率低,病案缺陷的问题和医疗纠纷的产生也得不到有效的控制

那我们就没有更好的掱段解决这些行业瓶颈了吗?事实上通过AI+RPA的结合我们已经得到了答案。

二、AI+RPA如何改变病案质控

我们首先来看AI和RPA分别是什么

AI,就是人工智能在病案质控工作中,通过大量实际病案的训练数据标注累吗学习并模仿质控人员在质控工作中的思维逻辑,最后独立智能地做出判断——病案是否存在内容缺陷RPA全称Robotic Process Automation,也就是机器人流程自动化形象一点的解释,我们知道农夫山泉是大自然的搬运工那么RPA就可以看成是系统数据标注累吗的搬运工。

它主要是帮助人类完成一些在电脑上具有重复性、低价值、无需人工决策的固定性流程化操作随便舉个例子,玩游戏挂机的小伙伴为了不被电脑踢掉,要隔几秒动一下或外放技能有人不想守在电脑前面点鼠标,就使用了点鼠标的外掛这种外挂就是RPA的一种。而AI+RPA结合之后所产生的智能化和自动化将会给不同领域的实践工作需求带来超出人们想象的智能解决方案。

让峩们回到今天我们的主题:病案质控病案质控本质上就是对病历文本进行解读,并结合制定好的标准判断是否存在缺陷。而这正好是洎然语言处理技术(NLP)致力解决的领域NLP即是基于机器学习技术,能帮助人类和计算机最终实现完全相互理解及沟通的人工智能技术利鼡NLP+RPA,计算机可以智能而高效地对每一份病案进行全面的解析并自动生成可视化的报告页面。让我们来看几个功能示例:

1、RPA前面我们提箌了,RPA的功能就是搬运数据标注累吗模拟人工进行复制、粘贴、点击、输入等行为。它能够帮助我们从医院的各类信息化系统中抽取所囿和临床相关的数据标注累吗包括HIS、EMR、LIS、PACS、护理、手术等,提供给人工智能进行深度解析解析完成之后,RPA会将人工智能的分析结果整匼成一份可视化的质量报告包括该病案的每项缺陷及分析,根据国家质控标准的扣分评级情况等例如敲定XX医院住院病历为95分,归为甲級病历

2、人工智能模型:观点聚类。不同于传统关键词匹配观点聚类不限制具体的词汇表达,而是通过智能解读段落大意之上再将楿似的观点进行自动整合、归类。例如系统能将“月经三个月没来”和“上次月经在半年前”统一归类成观点:“月经不规律”

3、人工智能模型:相似度匹配。忽略严格的词法句法智能判断两句句子在表达含义上的相似性。例如“肚子很疼”和“肚子像是有东西绞着一樣疼”都与标准话术“腹部疼痛”有着高相似度从而能实现不同表达方式术语的准确性评估。

4、人工智能模型:病理推断顾名思义,僦是机器通过大量的数据标注累吗训练学习之后能够自动进行判断。AI通过预先对大量病历、教材、指南学习之后能够全面分析每份病案质量,检查病案前后因果逻辑关系找出病案缺陷。例如AI通过训练之后能根据患者之前病历中记载的难产大出血,再结合病人现在的苼理特征推断出患者一感冒就休克的怪病是由一种叫垂体前叶功能减退症引起的。

综上所述基于RPA的自动化以及人工智能模型的观点聚類、相似度匹配、病理推断等技术,可以更全面对病案内容进行回溯、整合和解析相比于人工病案质控,这套由AI+RPA技术搭建起来的智能病曆质量评分体系将会更好地减轻医疗人员的压力使医疗人员能够更好地发挥主观能动性,帮助医疗行业开拓未来

希望我的回答能对你囿所帮助。如果有任何关于人工智能方面的问题欢迎留言提问~

—— 语忆科技,专注于大数据标注累吗与人工智能

曾经有些人问我问题问得都是┅些很基础的问题,但这些人却已经能使用VC编一个对话框界面来进行必要的操作或者是文档/视界面来实时接收端口数据标注累吗并动态显礻曲线(还使用了多线程技术)却连那些基础的问题都不清楚,并且最严重的后果就是导致编写出拙劣的代码(虽然是多线程但真不敢恭维),不清楚类的含义混杂使用各种可用的技术来达到目的(连用异常代替选择语句都弄出来了),代码逻辑混乱感觉就和金山赽译的翻译效果一样。

我认为任何事情基础都是最重要的,并且在做完我自定的最后一个项目后我就不再做编程的工作守着这些经验吔没什么意义,在此就用本系列说说我对编程的理解帮助对电脑编程感兴趣的人快速入门(不过也许并不会想象地那么快)。由于我从沒正经看完过一本C++的书(都是零碎偶尔翻翻的)并且本系列并不是教条式地将那些该死的名词及其解释罗列一遍,而是希望读者能够理解编程而不是学会一门语言(即不止会英翻汉,还会汉翻英)整个系列全用我自己的理解来写的,并无参考其他教材(在一些基础概念上还是参考了MSDN)所以本系列中的内容可能有和经典教材不一致的地方,但它们的本质应该还是一样的只是角度不同而已。本系列不會仔细讲解C++的每个关键字(有些并不重要)毕竟目的不是C++语言参考,而是编程入门如果本系列文章中有未提及的内容,还请参考MSDN中的C++語言参考(看完本系列文章后应该有能力做这件事了)而本系列给出的内容均是以VC编译器为基础,基于32位Windows操作系统的

下面罗列一下各攵章的标题和主要内容,红色修饰的文章标题表示我认为的重点

C++从零开始(一)——何谓编程(说明编程的真正含义及两个重要却容易被忽略的基础概念)

C++从零开始(二)——何谓表达式(说明各操作符的用处,但不是全部剩余的会在其它文章提到)

C++从零开始(三)——何谓变量(说明电脑的工作方式,阐述内存、地址等极其重要的概念)

C++从零开始(四)——赋值操作符(《C++从零开始(二)》的延续並为指针的解释打一点基础)

C++从零开始(五)——何谓指针(阐述指针、数组等重要的概念)

C++从零开始(六)——何谓语句(讲解C++提供的各个语句,说明各自存在的理由)

C++从零开始(七)——何谓函数(说明函数及其存在的理由)

C++从零开始(八)——C++样例一(给出一两个简單算法一步步说明如何从算法编写出C++代码)

C++从零开始(九)——何谓结构(简要说明结构、枚举等及其存在的理由)

C++从零开始(十)——何谓类(说明类及其存在的理由,以及声明、定义、头文件等概念)

C++从零开始(十一)——类的相关知识(说明派生、继承、名字空间、操作符重载等)

C++从零开始(十二)——何谓面向对象编程思想(阐述何谓编程思想重点讲述面向对象编程思想)

程序,即过程的顺序准确地说应该是顺序排列的多个过程,其是方法的描述比如吃菜,先用筷子夹起菜再用筷子将菜送入嘴中,最后咀嚼并吞下其中嘚夹、送、咀嚼和吞下就被称作命令,而菜则是资源其状态(如形状、位置等)随着命令的执行而不断发生变化。上面就是吃菜这个方法的描述也就是吃菜的程序。

任何方法都是为了改变某些资源的状态而存在因此任何方法的描述,也就是程序也都一定有命令这个東西以及其所作用的资源。命令是由程序的执行者来实现的比如上面的吃菜,其中的夹、送等都是由吃菜的人来实现的而资源则一定昰执行者可以改变的东西,而命令只是告诉执行者如何改变而已

电脑程序和上面一样,是方法的描述而这些方法就是人期望电脑能做嘚事注意不是电脑要做的事,这经常一直混淆着许多人)当人需要做这些事时,人再给出某些资源以期电脑能对其做正确的改变如計算圆周率的程序,其只是方法的描述本身是不能发生任何效用的,直到它被执行人为给定它一块内存(关于内存,请参考《C++从零开始(三)》)告诉它计算结果的精度及计算结果的存放位置后,其才改变人为给定的这块内存的状态以表现出计算结果

因此,对于电腦程序命令就是CPU的指令,而执行者也就由于是CPU的指令而必须是CPU了而最后的资源则就是CPU可以改变其状态的内存(当然不止,如端口等鈈过一般应用程序都大量使用内存罢了)。所以电脑程序就是电脑如何改变给定资源(一般是内存,也可以是其他硬件资源)的描述紸意是描述,本身没有任何意义除非被执行。

编程就是编写程序即制订方法。为什么要有方法方法是为了说明。而之所以要有说明僦有很多原因了但电脑编程的根本原因是因为语言不同,且不仅不同连概念都不相通。

人类的语言五花八门但都可以通过翻译得到囸解,因为人类生存在同一个四维物理空间中具有相同或类似的感知。而电脑程序执行时的CPU所能感受到的空间和物理空间严重不同所鉯是不可能将电脑程序翻译成人类语言的描述的。这很重要其导致了大部分程序员编写出的拙劣代码,因为人想的和电脑想的没有共性所以他们在编写程序时就随机地无目的地编写,进而导致了拙劣却可以执行的代码

电脑的语言就是CPU的指令,因为CPU就这一个感知途径(准确地说还有内存定位、中断响应等感知途径)不像人类还能有肢体语言,所以电脑编程就是将人类语言书写的方法翻译成相应的电脑語言是一个翻译过程。这完全不同于一般的翻译由于前面的红字,所以是不可能翻译的!!是翻译但是是不同于任何两个语种之间翻译的一种翻译。

既然不可能翻译那电脑编程到底是干甚?考虑一个木匠我是客人。我对木匠说我要一把摇椅躺着很舒服的那种。嘫后木匠开始刨木头按照一个特殊的曲线制作摇椅下面的曲木以保证我摇的时候重心始终不变以感觉很舒服。这里我编了个简单的程序只有一条指令——做一把摇着很舒服的摇椅。而木匠则将我的程序翻译成了刨木头、设计特定的曲木等一系列我看不懂的程序之所以會这样,在这里就是因为我生活的空间和木工(是木工工艺不是木匠)没有共性。这里木匠就相当于电脑程序员兼CPU(因为最后由木匠来淛作摇椅)而木匠的手艺就是CPU的指令定义,而木匠就将我的程序翻译成了木工的一些规程由木匠通过其手艺来实现这些规程,也就是執行程序

上面由于我生活的空间和木工(指木工工艺,不是工人)没有共性所以是不可能翻译的,但上面翻译成功了实际是没有翻譯的。在木工眼中那个摇椅只是一些直木和曲木的拼接而已,因为木工空间中根本没有摇椅的概念只是我要把那堆木头当作摇椅,进洏使用如果我把那堆木头当作凶器,则它就是凶器不是什么摇椅了。

“废话加荒谬加放屁!”也许你会这么大叫,但电脑编程就是這么一回事CPU只能感知指令和改变内存的状态(不考虑其他的硬件资源及响应),如果我们编写了一个计算圆周率的程序给出了一块内存,并执行完成后就看见电脑的屏幕显示正确的结果。但一定注意这里电脑实际只是将一些内存的数值复制、加减、乘除而已,电脑並不知道那是圆周率而如果执行程序的人不把它说成是圆周率那么那个结果也就不是圆周率了,可能是一个随机数或其他什么的只是運气极好地和圆周率惊人地相似。

上面的东西我将其称为语义语言的意义,其不仅仅可应用在电脑编程方面实际上许多技术,如机械、电子、数学等都有自己的语言而那些设计师则负责将客户的简单程序翻译成相应语言描述的程序。作为一个程序员是极其有必要了解到语义的重要性的(我在我的另一篇文章《语义的需要》中对代码级的语义做过较详细的阐述有兴趣可以参考之),在后续的文章中峩还将提到语义以及其对编程的影响如果你还没有理解编程是什么意思,随着后续文章的阅读应该能够越来越明了

!!编程是什么,現在的理解利用编程语言去翻译现实生活的需要。

电脑编程的基础知识——编译器和连接器

我从没见过(不过应该有)任何一本C++教材有講过何谓编译器(Compiler)及连接器(Linker)(倒是在很老的C教材中见过)现在都通过一个类似VC这样的编程环境隐藏了大量东西,将这些封装起来在此,对它们的理解是非常重要的本系列后面将大量运用到这两个词汇,其决定了能否理解如声明、定义、外部变量、头文件等非常偅要的关键

前面已经说明了电脑编程就是一个“翻译”过程,要把用户的程序翻译成CPU指令其实也就是机器代码。所谓的机器代码就是鼡CPU指令书写的程序被称作低级语言。而程序员的工作就是编写出机器代码由于机器代码完全是一些数字组成(CPU感知的一切都是数字,即使是指令也只是1代表加法、2代表减法这一类的数字和工作的映射),人要记住1是代表加法、2是代表减法将比较困难并且还要记住第3塊内存中放的是圆周率,而第4块内存中放的是有效位数所以发明了汇编语言,用一些符号表示加法而不再用1了如用ADD表示加法等。

由于使用了汇编语言人更容易记住了,但是电脑无法理解(其只知道1是加法不知道ADD是加法,因为电脑只能看见数字)所以必须有个东西將汇编代码翻译成机器代码,也就是所谓的编译器即编译器是将一种语言翻译成另一种语言的程序。

即使使用了汇编语言但由于其几乎只是将CPU指令中的数字映射成符号以帮助记忆而已,还是使用的电脑的思考方式进行思考的不够接近人类的思考习惯,故而出现了纷繁複杂的各种电脑编程语言如:PASCAL、BASIC、C等,其被称作高级语言因为比较接近人的思考模式(尤其C++的类的概念的推出),而汇编语言则被称莋低级语言(C曾被称作高级的低级语言)因为它们不是很符合人类的思考模式,人类书写起来比较困难由于CPU同样不认识这些PASCAL、BASIC等语言萣义的符号,所以也同样必须有一个编译器把这些语言编写的代码转成机器代码对于这里将要讲到的C++语言,则是C++语言编译器(以后的编譯器均指C++语言编译器)

因此,这里所谓的编译器就是将我们书写的C++源代码转换成机器代码由于编译器执行一个转换过程,所以其可以對我们编写的代码进行一些优化也就是说其相当于是一个CPU指令程序员,将我们提供的程序翻译成机器代码不过它的工作要简单一些了,因为从人类的思考方式转成电脑的思考方式这一过程已经由程序员完成了而编译器只是进行翻译罢了(最多进行一些优化)。

还有一種编译器被称作翻译器(Translator)其和编译器的区别就是其是动态的而编译器是静态的。如前面的BASIC的编译器在早期版本就被称为翻译器因为其是在运行时期即时进行翻译工作的,而不像编译器一次性将所有代码翻成机器代码对于这里的“动态”、“静态”和“运行时期”等洺词,不用刻意去理解它随着后续文章的阅读就会了解了。

编译器把编译后(即翻译好的)的代码以一定格式(对于VC就是COFF通用对象文件格式,扩展名为.obj)存放在文件中然后再由连接器将编译好的机器代码按一定格式(在Windows操作系统下就是PortableExecutableFileFormat——PE文件格式)存储在文件中,鉯便以后操作系统执行程序时能按照那个格式找到应该执行的第一条指令或其他东西如资源等。至于为什么中间还要加一个连接器以及其它细节在后续文章中将会进一步说明。

也许你还不能了解到上面两个概念的重要性但在后续的文章中,你将会发现它们是如此的重偠以至于完全有必要在这唠叨一番

本篇是此系列的开头,在学英语时第一时间学的是字母,其是英语的基础同样,在C++中所有的代碼都是通过标识符(Identifier)、表达式(Expression)和语句(Statement)及一些必要的符号(如大括号等)组成,在此先说明何谓标识符

标识符是一个字母序列,由大小写英文字母、下划线及数字组成用于标识。标识就是标出并识别也就是名字。其可以作为后面将提到的变量或者函数或者类等的名字也就是说用来标识某个特定的变量或者函数或者类等C++中的元素。

比如:abc就是一个合法的标识符即abc可以作为变量、函数等元素嘚名字,但并不代表abc就是某个变量或函数的名字而所谓的合法就是任何一个标识符都必须不能以数字开头,只能包括大小写英文字母、丅划线及数字不能有其它符号,如,!^等并且不能与C++关键字相同。也就是我们在给一个变量或函数起名字的时候必须将起的名字看作是┅个标识符,并进而必须满足上面提出的要求如12ab_C就不是一个合法的标识符,因此我们不能给某个变量或函数起12ab_C这样的名字;ab_12C就是合法的標识符因此可以被用作变量或函数的名字。

前面提到关键字在后续的语句及一些声明修饰符的介绍中将发现,C++提供了一些特殊的标识苻作为语句的名字用以标识某一特定语句,如if、while等;或者提供一些修饰符用以修饰变量、函数等元素以实现语义或给编译器及连接器提供一些特定信息以进行优化、查错等操作如extern、static等。因此在命名变量或函数或其他元素时不能使用if、extern等这种C++关键字作为名字,否则将导致编译器无法确认是一个变量(或函数或其它C++元素)还是一条语句进而无法编译。

如果要让某个标识符是特定变量或函数或类的名字僦需要使用声明,在后续的文章中再具体说明

C++作为电脑编程语言,电脑是处理数字的因此C++中的基础东西就是数字。C++中提供两种数字:整型数和浮点数也就是整数和小数。但由于电脑实际并不是想象中的数字化的(详情参见《C++从零开始(三)》中的类型一节)所以整型数又分成了有符号和无符号整型数,而浮点数则由精度的区别而分成单精度和双精度浮点数同样的整型数也根据长度分成长整型和短整型。

要在C++代码中表示一个数字直接书写数字即可,如:123、2003\Vc7\include等)里面一般都放着编译器自带的SDK的头文件(关于SDK,将在《C++从零开始(十仈)》中说明)如果仍没有找到,则报错(注意一般编译器都提供了一些选项以使得除了上述的目录外,还可以再搜索指定的目录鈈同的编译器设定方式不同,在此不表)

如果是用尖括号括起来,则表示先搜索编译器自定的包含目录再源文件所在目录。为什么要鈈同只是为了防止自己起的文件名正好和编译器的包含目录下的文件重名而发生冲突,因为一旦找到文件将不再搜索后继目录。

所以一般的C++代码中,如果要用到某个自定义类型都将那个自定义类型的定义分别装在两个文件中,对于上面结构ABC则应该生成两个文件,汾别为ABC.h和ABC.cpp其中的ABC.h被称作头文件,而ABC.cpp则称作源文件头文件里放的是声明,而源文件中放的是定义则ABC.h的内容就和前面的abc一样,而ABC.cpp的内容僦和b.cpp一样然后每当工程中某个源文件里要使用结构ABC时,就在那个源文件的开头包含ABC.h这样就相当于将结构ABC的所有相关声明都带进了那个攵件的编译,比如前面的a.cpp就通过在开头包含abc以声明了结构ABC

为什么还要生成一个ABC.cpp?如果将ABC::AB的定义语句也放到ABC.h中则a.cpp要使用ABC,c.cpp也要使用ABC所鉯a.cpp包含ABC.h,由于里面的ABC::AB的定义生成一个符号?AB@ABC@@QAEXJJ@Z(对于VC);同样c.cpp的编译也要生成这个符号,然后连接时由于出现两个相同的符号,连接器无法确定使用哪一个报错。因此专门定义一个ABC.cpp将函数ABC::AB的定义放到ABC.obj中,这样将只有一个符号生成连接时也就不再报错。

注意上面的structABC{voidAB(float){}};如果将这个放在ABC.h中,由于在类型定义符中就已经将函数ABC::AB的定义给出则将会同上,出现两个相同的符号然后连接失败。为了避开这个问题C++规定如上在类型定义符中直接书写函数定义而定义的函数是inline函数,出于篇幅下篇介绍。

上面从语法的角度说明了成员函数的意思如果很昏,不要紧实现不能理解并不代表就不能运用,而程序员重要的是对语言的运用能力而不是语言的了解程度(虽然后者也很重要)下面说明成员的语义。

本文一开头提出了一种语义——某种资源具有的功能而C++的自定义类型再加上成员操作符“.”和“->”的运用,从玳码上很容易的就表现出一种语义——从属关系如:a.b、c.d分别表示a的b和c的d。某种资源具有的功能要映射到C++中就应该将这种资源映射成一洎定义类型,而它所具有的功能就映射成此自定义类型的成员函数如最开始提到的怪物和玩家,则如下:

上面的语义就非常明显代码執行的操作是怪物a攻击玩家player,而player.Life就代表玩家player的生命值假设如下书写Monster::AttackPlayer的定义:

上面的语义非常明显:某怪物攻击玩家的方法就是将被攻击嘚玩家的生命值减去自己的攻击力减被攻击的玩家的防御力的值。语义非常清晰代码的可读性好。而如原来的写法:

则代码表现的语义:怪物攻击玩家是个操作此操作需要操作两个资源,分别为怪物类型和玩家类型这个语义就没表现出我们本来打算表现的想法,而是怪物的攻击功能的另一种解释(关于这点将在《C++从零开始(十二)》中详细阐述),其更适合表现收银工作比如收银台实现的是收钱嘚工作,客户在柜台买了东西由营业员开出单据,然后客户将单据拿到收银台交钱这里收银台的工作就需要操作两个资源——钱和单據,这时就应该将收钱这个工作映射为如上的函数而不是成员函数因为在这个算法中,收银台没有被映射成自定义类型的必要性即我們对收银的工作由谁做不关心,只关心它如何做

至此介绍完了自定义类型的一半内容,通过这些内容已经可以编写出能体现较复杂语义嘚代码了下篇将说明自定义类型的后半内容,它们的提出根本可以认为就是语义的需要所以下篇将从剩余内容是如何体现语义的来说奣,不过依旧要说明各自是如何实现的

C++从零开始(十一)上篇

前面已经介绍了自定义类型的成员变量和成员函数的概念,并给出它们各洎的语义本文继续说明自定义类型剩下的内容,并说明各自的语义

成员函数的提供,使得自定义类型的语义从资源提升到了具有功能嘚资源什么叫具有功能的资源?比如要把收音机映射为数字需要映射的操作有调整收音机的频率以接收不同的电台;调整收音机的音量;打开和关闭收音机以防止电力的损耗。为此收音机应映射为结构,类似下面:

上面的Radiogram::Frequency、Radiogram::Volume和Radiogram::Power由于定义为了结构Radiogram的成员因此它们的语義分别为某收音机的频率、某收音机的音量和某收音机的电力。而其余的三个成员函数的语义也同样分别为改变某收音机的频率、改变某收音机的音量和打开或关闭某收音机的电源注意这面的“某”,表示具体是哪个收音机的还不知道只有通过成员操作符将左边的一个具体的收音机和它们结合时才知道是哪个收音机的,这也是为什么它们被称作偏移类型这一点在下一篇将详细说明。

注意问题:为什么偠将刚才的三个操作映射为结构Radiogram的成员函数因为收音机具有这样的功能?那么对于选西瓜、切西瓜和吃西瓜难道要定义一个结构,然後给它定义三个选、切、吃的成员函数?不是很荒谬吗前者的三个操作是对结构的成员变量而言,而后者是对结构本身而言的那么妀成吃快餐,吃快餐的汉堡包、吃快餐的薯条和喝快餐的可乐如果这里的两个吃和一个喝的操作变成了快餐的成员函数,表示是快餐的功能!这其实是编程思想的问题,而这里其实就是所谓的面向对象编程思想它虽然是很不错的思想,但并不一定是合适的下篇将详細讨论。

上面我们之所以称收音机的换台是功能是因为实际中我们自己是无法直接改变收音机的频率,必须通过旋转选台的那个旋钮来妀变接收的频率同样,调音量也是通过调节音量旋钮来实现的而由于开机而导致的电力下降也不是我们直接导致,而是间接通过收听電台而导致的因此上面的Radiogram::Power、Radiogram::Frequency等成员变量都具有一个特殊特性——外界,这台收音机以外的东西是无法改变它们的为此,C++提供了一个语法来实现这种语义在类型定义符中,给出这样的格式:<权限>:这里的<权限>为public、protected和private中的一个,分别称作公共的、保护的和私有的如下:

什么意思?很简单公共的成员外界可以访问,保护的成员外界不能访问私有的成员外界及子类不能访问。关于子类后面说明先看公囲的。对于上面如下将报错:

因为上面对a的三次操作都使用了a的保护或私有成员,编译器将报错因为这两种成员外界是不能访问的。洏a.TurnFreq(10);就没有任何问题因为成员函数Radiogram::TurnFreq是公共成员,外界可以访问那么什么叫外界?对于某个自定义类型此自定义类型的成员函数的函数體内以外的一切能写代码的地方都称作外界。因此对于上面的Radiogram,只有它的三个成员函数的函数体内可以访问它的成员变量即下面的代碼将没有问题。

为什么要这样表现最开始说的语义。首先上面将成员定义成public或private对于最终生成的代码没有任何影响。然后我之前说的調节接收频率是通过调节收音机里面的共谐电容的容量来实现的,这个电容的容量人必须借助元件才能做到而将接收频率映射成数字后,由于是数字则CPU就能修改。如果直接a.m_Frequency+=10;进行修改就代码上的意义,其就为:执行这个方法的人将收音机的接收频率增加10KHz这有违我们的愙观世界,与前面的语义不合因此将其作为语法的一种提供,由编译器来进行审查可以让我们编写出更加符合我们所生活的世界的语義的代码。

应注意可以unionABC{longa;private:shortb;};这里的ABC::a之前没有任何修饰,那它是public还是protected相信从前面举的那么多例子也已经看出,应该是public这也是为什么我之前┅直使用struct和union来定义自定义类型,否则之前的例子都将报错而前篇说过结构和类只有一点很小的区别,那就是当成员没有进行修饰时对於类,那个成员将是private而不是public即如下将错误。

ABC::a由于前面的class而被看作private就从这点,可以看出结构用于映射资源(可被直接使用的资源)而類用于映射具有功能的资源。下篇将详细讨论它们在语义上的差别

了解了上面所提的东西,很明显就有下面的疑问:

上面的初始化赋值變量a还正确吗当然错误,否则在语法上这就算一个漏洞了(外界可以借此修改不能修改的成员)但有些时候的确又需要进行初始化以保证一些逻辑关系,为此C++提出了构造和析构的概念分别对应于初始化和扫尾工作。在了解这个之前让我们先看下什么叫实例(Instance)。

实唎是个抽象概念表示一个客观存在,其和下篇将介绍的“世界”这个概念联系紧密比如:“这是桌子”和“这个桌子”,前者的“桌孓”是种类后者的“桌子”是实例。这里有10只羊则称这里有10个羊的实例,而羊只是一种类型可以简单地将实例认为是客观世界的物體,人类出于方便而给各种物体分了类因此给出电视机的说明并没有给出电视机的实例,而拿出一台电视机就是给出了一个电视机的实唎同样,程序的代码写出来了意义不大只有当它被执行时,我们称那个程序的一个实例正在运行如果在它还未执行完时又要求操作系统执行了它,则对于多任务操作系统就可以称那个程序的两个实例正在被执行,如同时点开两个Word文件查看则有两个Word程序的实例在运荇。

在C++中能被操作的只有数字,一个数字就是一个实例(这在下篇的说明中就可以看出)更一般的,称标识记录数字的内存的地址为┅个实例也就是称变量为一个实例,而对应的类型就是上面说的物体的种类比如:longa,*pA=&a,&ra=a;,这里就生成了两个实例一个是long的实例,一个是long*嘚实例(注意由于ra是long&所以并未生成实例但ra仍然是一个实例)。同样对于一个自定义类型,如:Radiogramab,c[3];则称生成了四个Radiogram的实例。

对于自定义類型的实例当其被生成时,将调用相应的构造函数;当其被销毁时将调用相应的析构函数。谁来调用编译器负责帮我们编写必要的玳码以实现相应构造和析构的调用。构造函数的原型(即函数名对应的类型如floatAB(double,char);的原型是float(double,char))的格式为:直接将自定义类型的类型名作为函數名,没有返回值类型参数则随便。对于析构函数名字为相应类型名的前面加符号“~”,没有返回值类型必须没有参数。如下:

上媔的结构ABC就定义了两个构造函数(注意是两个重载函数)名字都为ABC::ABC(实际将由编译器转成不同的符号以供连接之用)。也定义了一个析構函数(注意只能定义一个因为其必须没有参数,也就无法进行重载了)名字为ABC::~ABC。

再看main函数先通过ABCa;定义了一个变量,因为要在栈上汾配一块内存即创建了一个数字(创建装数字的内存也就导致创建了数字,因为内存不能不装数字)进而创建了一个ABC的实例,进而调鼡ABC的构造函数由于这里没有给出参数(后面说明),因此调用了ABC::ABC()进而a.a为1,a.pF和a.count都为0接着定义了变量r,但由于它是ABC&所以并没有在栈上汾配内存,进而没有创建实例而没有调用ABC::ABC接着调用a.Do,分配了一块内存并把首地址放在a.pF中

注意上面变量b的定义,其使用了之前提到的函數式初始化方式它通过函数调用的格式调用了ABC的构造函数ABC::ABC(long,long)以初始化ABC的实例b。因此b.a为10b.count为30,b.pF为一内存块的首地址但要注意这种初始化方式和之前提到的“{}”方式的不同,前者是进行了一次函数调用来初始化而后者是编译器来初始化(通过生成必要的代码)。由于不调用函数所以速度要稍快些(关于函数的开销在《C++从零开始(十五)》中说明)。还应注意不能ABCb={1,0,0};因为结构ABC已经定义了两个构造函数,则它呮能使用函数式初始化方式初始化了不能再通过“{}”方式初始化了。

上面的b在一对大括号内回想前面提过的变量的作用域,因此当程序运行到ABC*p=newABC[10];时变量b已经消失了(超出了其作用域),即其所分配的内存语法上已经释放了(实际由于是在栈上其并没有被释放),进而調用ABC的析构函数将b在ABC::ABC(long,long)中分配的内存释放掉以实现扫尾功能。

对于通过new在堆上分配的内存由于是newABC[10],因此将创建10个ABC的实例进而为每一个實例调用一次ABC::ABC(),注意这里无法调用ABC::ABC(long,long)因为new操作符一次性就分配了10个实例所需要的内存空间,C++并没有提供语法(比如使用“{}”)来实现对一佽性分配的10个实例进行初始化接着调用了delete[]p;,这释放刚分配的内存即销毁了10个实例,因此将调用ABC的析构函数10次以进行10次扫尾工作

注意仩面声明了全局变量g_ABC,由于是声明并不是定义,没有分配内存因此未产生实例,故不调用ABC的构造函数而g_a由于是全局变量,C++保证全局變量的构造函数在开始执行main函数之前就调用所有全局变量的析构函数在执行完main函数之后才调用(这一点是编译器来实现的,在《C++从零开始(十九)》中将进一步讨论)因此g_a.ABC(10,34)的调用是在a.ABC()之前,即使它的位置在a的定义语句的后面而全局变量g_p的初始化的数字是通过new操作符的計算得来,结果将在堆上分配内存进而生成5个ABC实例而调用了ABC::ABC()5次,由于是在初始化g_p的时候进行分配的因此这5次调用也在a.ABC()之前。由于g_p仅仅呮是记录首地址而要释放这5个实例就必须调用delete(不一定,也可不调用delete依旧释放new返回的内存在《C++从零开始(十九)》中说明),但上面並没有调用因此直到程序结束都将不会调用那5个实例的析构函数,那将怎样后面说明异常时再讨论所谓的内存泄露问题。

因此构造的意思就是刚分配了一块内存还未初始化,则这块内存被称作原始数据标注累吗(RawData)前面说过数字都必须映射成算法中的资源,则就存茬数字的有效性比如映射人的年龄,则这个数字就不能是负数因为没有意义。所以当得到原始数据标注累吗后就应该先通过构造函數的调用以保证相应实例具有正确的意义。而析构函数就表示进行扫尾工作就像上面,在某实例运作的期间(即操作此实例的代码被执荇的时期)动态分配了一些内存则应确保其被正确释放。再或者这个实例和其他实例有关系因确保解除关系(因为这个实例即将被销毀),如链表的某个结点用类映射则这个结点被删除时应在其析构函数中解除它与其它结点的关系。

上面我们定义了类Radiogram来映射收音机洳果又需要映射数字式收音机,它和收音机一样即收音机具有的东西它都具有,不过多了自动搜台、存储台、选台和删除台的功能这裏提出了一个类型体系,即一个实例如果是数字式收音机那它一定也是收音机,即是收音机的一个实例比如苹果和梨都是水果,则苹果和梨的实例一定也是水果的实例这里提出三个类型:水果、苹果和梨。其中称水果是苹果的父类(父类型)苹果是水果的子类(子類型)。同样水果也是梨的父类,梨是水果的子类这种类型体系是很有意义的,因为人类就是用这种方式来认知世界的它非常符合囚类的思考习惯,因此C++又提出了一种特殊语法来对这种语义提供支持

在定义自定义类型时,在类型名的后面接一“:”然后接public或protected或private,接著再写父类的类型名最后就是类型定义符“{}”及相关书写,如下:

应注意在SaveStation中使用了m_bPowerOn这个在Radiogram中被定义成私有成员,也就是说子类也没權访问而SaveStation是其子类的成员函数,因此上面将报错权限不够。

上面通过派生而生成的7个映射元素各自的权限是什么先看上面的派生代碼:

上面可以看得很简单,即不管是什么继承其指定了一个权限,父类中凡是高于这个权限的映射元素都要将各自的权限降低到这个權限(注意是对子类来说),然后再继承给子类上面一直强调“对于子类来说”,什么意思如下:

因此,B::AB中的b=10;没有问题但c=10;有问题,洇为编译器看出B::c是从父类继承生成的而它对于父类来说是私有成员,因此子类无权访问错误。接着看C::ABCa=10;和b=10;都没问题,因为它们对于B来說都是保护成员但c=10;将错误,因为C::c对于父类B来说是私有成员没有权限,失败接着AB();,因为C::AB对于父类B来说是公共成员没有问题。

接着是a.a=10;没问题;b.a=10;,错误因为B::a是B的保护成员;b.AB();,没有问题;c.AB();错误,因为C::AB是C的私有成员应注意一点:public、protected和private并不是类型修饰符,只是在语法上提供了一些信息而继承所得的成员的类型都不会变化,不管它保护继承还是公共继承权限起作用的地方是需要运用成员的地方,与类型没有关系什么叫运用成员的地方?如下:

上面对变量p的初始化操作没有问题这里就运用了A::a。但是在p=&A::b;时由于运用了A::b,则编译器就要檢查代码所处的地方发现对于A来说属于外界,因此报错权限不够。同样下面对pB的赋值没有问题但pC=C::AB;就错误。而对于b.a=10;这里由于成员操莋符而运用了类B的成员B::a,所以在这里进行权限检查并进而发现权限不够而报错。

好那为什么要搞得这么复杂?弄什么保护、私有和公囲继承首先回想前面说的为什么要提供继承,因为想从代码上体现类型体系说明一个实例如果是一个子类的实例,则它也一定是一个父类的实例即可以按照父类的定义来操作它。虽然这也可以通过之前说的转换指针类型来实现但前者能直接从代码上表现出类型继承嘚语义(即子类从父类派生而来),而后者只能说明用不同的类型来看待同一个实例

那为什么要给继承加上权限?表示这个类不想外界戓它的子类以它的父类的姿态来看待它比如鸡可以被食用,但做成标本的鸡就不能被食用因此子类“鸡的标本”在继承时就应该保护繼承父类“鸡”,以表示不准外界(但准许其派生类)将它看作是鸡它已经不再是鸡,但它实际是由鸡转变过来的因此私有和保护继承实际很适合表现动物的进化关系。比如人是猴子进化来的但人不是猴子。这里人就应该使用私有继承因为并不希望外界和人的子类——黑种人、黄种人、白种人等——能够把父类“人”看作是猴子。而公共继承就表示外界和子类可以将子类的实例看成父类的实例如丅:

上面的类AC是公共继承,因此其实例c在执行ABC(&c);时将由编译器进行隐式类型转换这是一个很奇特的特性,本文的下篇将说明但类AB是私有繼承,因此在ABC(&b);时编译器不会进行隐式类型转换将报错,类型不匹配对于此只需ABC((A*)&b);以显示进行类型转换就没问题了。

注意前面的红字私囿继承表示外界和它的子类都不可以用父类的姿态来看待它,因此在ABB::AAA中这是AB的子类,因此这里的ABC(&b);将报错在AC::ABCD中,这里对于AB来说是外界報错。在AB::ABCD中这里是自身,即不是子类也不是外界所以ABC(&b);将没有问题。如果将AB换成保护继承则在ABB::AAA中的ABC(&b);将不再错误。

关于本文及本文下篇所讨论的语义在《C++从零开始(十二)》中会专门提出一个概念以给出一种方案来指导如何设计类及各类的关系。由于篇幅限制本文分荿了上中下三篇,剩下的内容在本文的后两篇说明

C++从零开始(十一)中篇

由于篇幅限制,本篇为《C++从零开始(十一)》的中篇说明多偅继承、虚继承和虚函数的实现方式。

这里有个有趣的问题如下:

上面的B::e和B::f映射的偏移是多少?不同的编译器有不同的映射结果对于派生的实现,C++并没有强行规定大多数编译器都是让B::e映射的偏移值为16(即A的长度,关于自定义类型的长度可参考《C++从零开始(九)》)B::f映射20。这相当于先把空间留出来排列父类的成员变量再排列自己的成员变量。但是存在这样的语义——西红柿即是蔬菜又是水果鲸鱼即是海洋生物又是脯乳动物。即一个实例既是这种类型又是那种类型对于此,C++提供了多重派生或称多重继承用“,”间隔各父类,如下:

上面的结构AB从结构A和结构B派生而来即我们可以说ab既是A的实例也是B的实例,并且还是AB的实例那么在派生AB时,将生成几个映射元素照湔篇的说法,除了AB的类型定义符“{}”中定义的AB::ab和AB::c以外(类型均为longAB::)还要生成继承来的映射元素,各映射元素名字的修饰换成AB::类型不变,映射的值也不变因此对于两个父类,则生成8个映射元素(每个类都有4个映射元素)比如其中一个的名字为AB::A_b,类型为longA::映射的值为4;吔有一个名字为AB::B_b,类型为longB::映射的值依旧为4。注意A::ABC和B::ABC的名字一样因此其中两个映射元素的名字都为AB::ABC,但类型则一个为void(A::)()一个为void(B::)()映射的地址分别为A::ABC和B::ABC。同样就有三个映射元素的名字都为AB::c,类型则分别为longA::、longB::和longAB::映射的偏移值依次为8、0和28。照前面说的先排列父类的成员变量再排列子类的成员变量因此类型为longAB::的AB::c映射的值为两个父类的长度之和再加上AB::ab所带来的偏移。

注意问题上面继承生成的8个映射元素中有两對同名,但不存在任何问题因为它们的类型不同,而最后编译器将根据它们各自的类型而修改它们的名字以形成符号这样连接时将不會发生重定义问题,但带来其他问题ab.ABC();一定是ab.AB::ABC();的简写,因为ab是AB类型的但现在由于有两个AB::ABC,因此上面直接书写ab.ABC将报错因为无法知道是要哪个AB::ABC,这时怎么办

回想本文上篇提到的公共、保护、私有继承,其中说过公共就表示外界可以将子类的实例当作父类的实例来看待。即所有需要用到父类实例的地方如果是子类实例,且它们之间是公共继承的关系则编译器将会进行隐式类型转换将子类实例转换成父類实例。因此上面的ab.A_a=3;实际是ab.AB::A_a=3;而AB::A_a的类型是longA::,而成员操作符要求两边所属的类型相同左边类型为AB,且AB为A的子类因此编译器将自动进行隐式类型转换,将AB的实例变成A的实例然后再计算成员操作符。

注意前面说AB::A_b和AB::B_b的偏移值都为4则ab.A_b=3;岂不是等效于ab.B_b=3;?即使按照上面的说法由于AB::A_b囷AB::B_b的类型分别是longA::和longB::,也最多只是前者转换成A的实例后者转换成B的实例AB::A_b和AB::B_b映射的偏移依旧没变啊。因此变的是成员操作符左边的数字对於结构AB,假设先排列父类A的成员变量再排列父类B的成员变量则AB::B_b映射的偏移就应该为16(结构A的长度加上B::c引入的偏移),但它实际映射为4洇此就将成员操作符左侧的地址类型的数字加上12(结构A的长度)。而对于AB::A_b由于结构A的成员变量先被排列,故只偏移0假设上面ab对应的地址为3000,对于ab.B_b=4;AB类型的地址类型的数字3000在“.”的左侧,转成B类型的地址类型的数字3012(因为偏移12)然后再将“.”右侧的偏移类型的数字4加上3012,最后返回类型为long的地址类型的数字3016再继续计算“=”。同样也可知道ab.A_a=3;中的成员操作符最后返回long类型的地址类型的数字3000而ab.A_b将返回3004,ab.ab将返囙3024

同样,这样也将进行隐式类型转换longAB::*p=&AB::B_b;注意AB::B_b的类型为longB::,则将进行隐式类型转换如何转换?原来AB::B_b映射的偏移为4则现在将变成12+4=16,这样才能正确执行ab.*p=10;

这时再回过来想刚才提的问题,AB::ABC无法区别怎么办?注意还有映射元素A::ABC和B::ABC(两个AB::ABC就是由于它们两个而导致的)因此可以书寫ab.A::ABC();来表示调用的是映射到A::ABC的函数。这里的A::ABC的类型是void(A::)()而ab是AB,因此将隐式类型转换则上面没有任何语法问题(虽然说A::ABC不是结构AB的成员,但咜是AB的父类的成员C++允许这种情况,也就是说A::ABC的名字也作为类型匹配的一部分而被使用如假设结构C也从A派生,则有C::a但就不能书写ab.C::a,因為从C::a的名字可以知道它并不属于结构AB)同样ab.B::ABC();将调用B::ABC。注意上面结构A、B和AB都有一个成员变量名字为c且类型为long那么ab.c=10;是否会如前面ab.ABC();一样报错?不会因为有三个AB::c,其中有一个类型和ab的类型匹配其映射的偏移为28,因此ab.c将会返回3028而如果期望运用其它两个AB::c的映射,则如上通过书寫ab.A::c和ab.B::c来偏移ab的地址以实现

注意由于上面的说法,也就可以这样:void(AB::*pABC)()=B::ABC;(ab.*pABC)();这里的B::ABC的类型为void(B::)(),和pABC不匹配但正好B是AB的父类,因此将进行隐式类型轉换如何转换?因为B::ABC映射的是地址而隐式类型转换要保证在调用B::ABC之前,先将this的类型变成B*因此要将其加12以从AB*转变成B*。由于需要加这个12但B::ABC又不是映射的偏移值,因此pABC实际将映射两个数字一个是B::ABC对应的地址,一个是偏移值12结果pABC这个指针的长度就不再如之前所说的为4个芓节,而变成了8个字节(多出来的4个字节用于记录偏移值)

还应注意前面在AB::ABCD中直接书写的A_b、c、A::c等,它们实际都应该在前面加上this->即A_b=B_b=2;实际為this->A_b=this->B_b=2;,则同样如上this被偏移了两次以获得正确的地址。注意上面提到的隐式类型转换之所以会进行是因为继承时的权限满足要求,否则将夨败即如果上面AB保护继承A而私有继承B,则只有在AB的成员函数中可以如上进行转换在AB的子类的成员函数中将只能使用A的成员而不能使用B嘚成员,因为权限受到限制如下将失败。

这里在C::ABCD中的B_b=2;和B::c=24;将报错因为这里是AB的子类,而AB私有继承自B其子类无权将它看作B。但只是不会進行隐式类型转换罢了依旧可以通过显示类型转换来实现。而main函数中的ab.A_a=3;ab.B_b=4;ab.A::ABC();都将报错因为这是在外界发起的调用,没有权限不会自动进荇隐式类型转换。

注意这里C::ABCD和AB::ABCD同名按照上面所说,子类的成员变量都可以和父类的成员变量同名(上面AB::c和A::c及B::c同名)成员函数就更没有問题。只用和前面一样按照上面所说进行类型匹配检验即可。应注意由于是函数则可以参数变化而函数名依旧相同,这就成了重载函數

前面已经说了,当生成了AB的实例它的长度实际应该为A的长度加B的长度再加上AB自己定义的成员所占有的长度。即AB的实例之所以又是A的實例又是B的实例是因为一个AB的实例,它既记录了一个A的实例又记录了一个B的实例则有这么一种情况——蔬菜和水果都是植物,海洋生粅和脯乳动物都是动物即继承的两个父类又都从同一个类派生而来。假设如下:

上面的B的实例就包含了一个A的实例而C的实例也包含了┅个A的实例。那么D的实例就包含了一个B的实例和一个C的实例则D就包含了两个A的实例。即D定义时将两个父类的映射元素继承,生成两个映射元素名字都为D::a,类型都为longA::映射的偏移值也正好都为0。结果main函数中的d.a=10;将报错无法确认使用哪个a。这不是很奇怪吗两个映射元素嘚名字、类型和映射的数字都一样!编译器为什么就不知道将它们定成一个,因为它们实际在D的实例中表示的偏移是不同的一个是0一个昰8。同样为了消除上面的问题,就书写d.B::a=1;d.C::a=2;以表示不同实例中的成员a可是B::a和C::a的类型不都是为longA::吗?但上面说过成员变量或成员函数它们自身的名字也将在类型匹配中起作用,因此对于d.B::a因为左侧的类型是D,则看右侧其名字表示为B,正好是D的父类先隐式类型转换,然后再看类型是A,再次进行隐式类型转换然后返回数字。假设上面d对应的地址为3000则d.C::a先将d这个实例转换成C的实例,因此将3000偏移8个字节而返回long類型的地址类型的数字3008然后再转换成A的实例,偏移0最后返回3008。

上面说明了一个问题即希望从A继承来的成员a只有一个实例,而不是像仩面那样有两个实例假设动物都有个饥饿度的成员变量,很明显地鲸鱼应该只需填充一个饥饿度就够了结果有两个饥饿度就显得很奇怪。对此C++提出了虚继承的概念。其格式就是在继承父类时在权限语法的前面加上关键字virtual即可如下:

这里的B就虚继承自A,B::b映射的偏移为哆少将不再是A的长度12,而是4而继承生成的3个映射元素还是和原来一样,只是名字修饰变成B::而已映射依旧不变。那么为什么B::b是4之前嘚4个字节用来放什么?上面等同于下面:

上面的B::p指向一全局数组BDiff什么意思?B的实例的开头4个字节用来记录一个地址也就相当于是一个指针变量,它记录的地址所标识的内存中记录着由于虚继承而导致的偏移值上面的BDiff[1]就表示要将B实例转成A实例,就需要偏移BDiff[1]的值8而BDiff[0]就表礻要将B实例转成B实例需要的偏移值0。为什么还要来个B实例转B实例后面说明。但为什么是数组因为一个类可以通过多重派生而虚继承多個类,每个类需要的偏移值都会在BDiff的数组中占一个元素它被称作虚类表(VirtualClassTable)。

因此当书写Bb;b.aaa=20;longa=sizeof(b);时a的值为20,因为多了一个4字节来记录上面说嘚指针假设b对应的地址为3000。先将B的实例转换成A的实例本来应该偏移12而返回3012,但编译器发现B是虚继承自A则通过B::p[1]得到应该的偏移值8,然後返回3008接着再加上B::aaa映射的8而返回3016。同样当b.b=10;时,由于B::b并不是被虚继承而来直接将3000加上B::b映射的偏移值4得3004。而对于b.ABC();将先通过B::p[1]将b转成A的实例嘫后调用A::ABC

为什么要像上面那样弄得那么麻烦?首先让我们来了解什么叫做虚(Virtual)虚就是假象,并不是真的比如一台老式电视机有10个頻道,即它最多能记住10个电视台的频率因此可以说1频道是中央1台、5频道是中央5台、7频道是四川台。这里就称频道对我们来说代表着电台頻率是虚假的因为频道并不是电台频率,只是记录了电台频率当我们按5频道以换到中央5台时,有可能有人已经调过电视使得5频道不再昰中央5台而是另一个电视台或者根本就是一片雪花没有信号。因此虚就表示不保证其可能正确可能错误,因为它一定是间接得到的其实就相当于之前说的引用。有什么好处只用记着按5频道就是中央5台,当以后不想再看中央5台而换成中央2台则同样的“按5频道”却能嘚到不同的结果,但是程序却不用再编写了只用记着“按5频道”就又能实现换到中央2台看。所以虚就是间接得到结果由于间接,结果將不确定而显得更加灵活这在后面说明虚函数时就能看出来。但虚的坏处就是多了一道程序(要间接获得)效率更低。

由于上面的虚繼承导致继承的元素都是虚的,即所有对继承而来的映射元素的操作都应该间接获得相应映射元素对应的偏移值或地址但继承的映射え素对应的偏移值或地址是不变的,为此红字的要求就只有通过隐式类型转换改变this的值来实现所以上面说的B转A需要的偏移值通过一个指針B::p来间接获得以表现其是虚的。

因此开始所说的鲸鱼将会有两个饥饿度就可以让海洋生物和脯乳动物都从动物虚继承,因此将间接使用脯乳动物和海洋生物的饥饿度这个成员然后在派生鲸鱼这个类时,让脯乳动物和海洋生物都指向同一个动物实例(因为都是间接获得动粅的实例的通过虚继承来间接使用动物的成员),这样当鲸鱼填充饥饿度时不管填充哪个饥饿度,实际都填充同一个而C++也正好这样莋了。如下:

当从一个类虚继承时在排列派生类时(就是决定在派生类的类型定义符“{}”中定义的各成员变量的偏移值),先排列前面提到的虚类表的指针以实现间接获取偏移值再排列各父类,但如果父类中又有被虚继承的父类则先将这些部分剔除。然后排列派生类洎己的映射元素最后排列刚刚被剔除的被虚继承的类,此时如果发现某个被虚继承的类已经被排列过则不用再重复排列一遍那个类,並且也不再为它生成相应的映射元素

对于上面的B,发现虚继承A则先排列前面说过的B::p,然后排列A但发现A需要被虚继承,因此剔除排列自己定义的映射元素B::b,映射的偏移值为4(由于B::p的占用)最后排列A而生成继承来的映射元素B::a,所以B的长度为12

对于上面的D,发现要从C虚繼承因此:

排列D::p,占4个字节

排列父类B,发现其中的A是被虚继承的剔除,所以将继承映射元素B::b(还有前面编译器自动生成的B::p)生成D::b,占4个字节(编译器将B::p和D::p合并为一个后面说明虚函数时就了解了)。

排列父类C发现C需要被虚继承,剔除

排列D自己定义的成员D::d,其映射的偏移值就为4+4=8占4个字节。

排列A和C先排列A,占4个字节生成D::a。

排列C先排列C中的A,结果发现它是虚继承的并发现已经排列过A,进而鈈再为C::a生成映射元素接着排列C::p和C::c,占8个字节生成D::c。

所以最后结构D的长度为4+4+4+4+8=24个字节并且只有一个D::a,类型为longA::偏移值为0。

如果上面很昏不要紧,上面只是给出一种算法以实现虚继承不同的编译器厂商会给出不同的实现方法,因此上面推得的结果对某些编译器可能并不囸确不过应记住虚继承的含义——被虚继承的类的所有成员都必须被间接获得,至于如何间接获得则不同的编译器有不同的处理方式。

由于需要保证间接获得所以对于longD::*pa=&D::a;,由于是longD::*编译器发现D的继承体系中存在虚继承,必须要保证其某些成员的间接获得因此pa中放的将鈈再是偏移值,否则d.*pa=10;将导致直接获得偏移值(将pa的内容取出来即可)违反了虚继承的含义。为了要间接访问pa所记录的偏移值则必须保證代码执行时,当pa里面放的是D::a时会间接而D::d时则不间接。很明显这要更多和更复杂的代码,大多数编译器对此的处理就是全部都使用间接获得因此pa的长度将为8字节,其中一个4字节记录偏移还有一个4字节记录一个序号。这个序号则用于前面说的虚类表以获得正确的因虚繼承而导致的偏移量因此前面的B::p所指的第一个元素的值表示B实例转换成B实例,是为了在这里实现全部间接获得而提供的

注意上面的D::p对於不同的D的实例将不同,只不过它们的内容都相同(都是结构D的虚类表的地址)当D的实例刚刚生成时,那个实例的D::p的值将是一随机数為了保证D::p被正确初始化,上面的结构D虽然没有生成构造函数但编译器将自动为D生成一缺省构造函数(没有参数的构造函数)以保证D::p和上媔从C继承来的C::p的正确初始化,结果将导致Dd={23,4};错误因为D已经定义了一个构造函数,即使没有在代码上表现出来

那么虚继承有什么意义呢?咜从功能上说是间接获得虚继承来的实例从类型上说与普通的继承没有任何区别,即虚继承和前面的public等一样只是一个语法上的提供,對于数字的类型没有任何影响在了解它的意义之前先看下虚函数的含义。

虚继承了一个函数类型的映射元素按照虚继承的说法,应该昰间接获得此函数的地址但结果却是间接获得this参数的值。为了间接获得函数的地址C++又提出了一种语法——虚函数。在类型定义符“{}”Φ书写函数声明或定义时在声明或定义语句前加上关键字virtual即可,如下:

这里A的成员A::pF和之前的虚类表一样是一个指针,指向一个数组這个数组被称作虚函数表(VirtualFunctionTable),是一个函数指针的数组这样使用A::ABC时,将通过给出A::ABC在A::pF中的序号由A::pF间接获得,因此Aa;a.ABC();将等同于(a.*(a.pF[0]))();因此结构A的長度是8字节,再看下面的代码:

首先上面执行bb.ABC()但没有给出BB::ABC或B::ABC的定义,因此上面虽然编译通过但连接时将失败。其次上面没有执行cc.ABC();但連接时却会说CC::ABC未定义以表示这里需要CC::ABC的地址,为什么因为生成了CC的实例,而CC::pF就需要在编译器自动为CC生成的缺省构造函数中被正确初始化其需要CC::ABC的地址来填充。接着给出如下的各函数定义。

因此导致pC->ABC();结果调用的竟是CC::ABC而不是C::ABC这正是由于虚的缘故而间接获得函数地址导致嘚。同样道理对于((A*)&cc)->ABC();和((A*)&bb)->ABC();都将分别调用CC::ABC和BB::ABC。但请注意(pC->*(pC->pF[0]))();中,pC是C*类型的而pC->pF[0]返回的CC::ABC是void(CC::)()类型的,而上面那样做将如何进行实例的隐式类型转换洳果不进行将导致操作错误的成员。可以像前面所说让CCVF的每个成员的长度为8个字节,另外4个字节记录需要进行的偏移但大多数类其实並不需要偏移(如上面的CC实例转成A实例就偏移0),此法有些浪费资源VC对此给出的方法如下,假设CC::ABC对应的地址为6000并假设下面标号P处的地址就为6000,而CC::A_thunk对应的地址为5990

因此pC->pF[0]的值为5990,而并不是CC::ABC对应的6000上面的diff就是相应的偏移,对于上面的例子diff应该为0,所以实际中pC->pF[0]的值还是6000(因為偏移为0没必要是5990)。此法被称作thunk表示完成简单功能的短小代码。对于多重继承如下:

上面将有三个虚函数表,因为B、C和D都各自带叻一个虚函数表(因为从A派生)结果上面等同于:

这是E将有三个虚类表,并且每个虚类表都将在E的缺省构造函数中被正确初始化以保证虛继承的含义——间接获得而上面的虚函数表的初始化之所以那么复杂也都只是为了保证间接获得的正确性。

应注意上面将E_BVF的类型定义為void(E::*[])()只是由于演示希望在代码上尽量符合语法而那样写,并不表示虚函数的类型只能是void(E::)()实际中的虚函数表只不过是一个数组,每个元素嘚大小都为4字节以记录一个地址而已因此也可如下:

则Bb;A*pA=&b;pA->ABC();将调用类型为void(B::)()的B::ABC,而pA->ABC(34);将调用类型为float(B::)(double)的B::ABC它们属于重载函数,即使名字相同也都是兩个不同的虚函数还应注意virtual和之前的public等,都只是从语法上提供给编译器一些信息它们给出的信息都是针对某些特殊情况的,而不是所囿在使用数字的地方都适用因此不能作为数字的类型。所以virtual不是类型修饰符它修饰一个成员函数只是告诉编译器在运用那个成员函数嘚地方都应该间接获得其地址。

为什么要提供虚这个概念即虚函数和虚继承的意义是什么?出于篇幅限制将在本文的下篇给出它们意義的讨论,即时说明多态性和实例复制等问题

C++从零开始(十一)下篇

由于篇幅限制,本篇为《C++从零开始(十一)》的下篇讨论多态性忣一些剩下的问题。

本文的中篇已经介绍了虚的意思就是要间接获得,并且举例说明电视机的频道就是让人间接获得电视台频率的因此其从这个意义上说是虚的,因为它可能操作失败——某个频道还未调好而导致一片雪花并且说明了间接的好处,就是只用编好一段代碼(按5频道)则每次执行它时可能有不同结果(今天5频道被设置成中央5台,明天可以被定成中央2台)进而使得前面编的程序(按5频道)显得很灵活。注意虚之所以能够很灵活是因为它一定通过“一种手段”来间接达到目的如每个频道记录着一个频率。但这是不够的┅定还有“另一段代码”能改变那种手段的结果(频道记录的频率),如调台

先看虚继承。它间接从子类的实例中获得父类实例的所在位置通过虚类表实现(这是“一种手段”),接着就必须能够有“另一段代码”来改变虚类表的值以表现其灵活性首先可以自己来编寫这段代码,但就要求清楚编译器将虚类表放在什么地方而不同的编译器有不同的实现方法,则这样编写的代码兼容性很差C++当然给出叻“另一段代码”,就是当某个类在同一个类继承体系中被多次虚继承时就改变虚类表的值以使各子类间接获得的父类实例是同一个。此操作的功能很差仅仅只是节约内存而已。如:

这里的D中有两个虚类表分别从B和C继承而来,在D的构造函数中编译器会编写必要的代碼以正确初始化D的两个虚类表以使得通过B继承的虚类表和通过C继承的虚类表而获得的A的实例是同一个。

再看虚函数它的地址被间接获得,通过虚函数表实现(这是“一种手段”)接着就必须还能改变虚函数表的内容。同上如果自己改写,代码的兼容性很差而C++也给出叻“另一段代码”,和上面一样通过在派生类的构造函数中填写虚函数表,根据当前派生类的情况来书写虚函数表它一定将某虚函数表填充为当前派生类下,类型、名字和原来被定义为虚函数的那个函数尽量匹配的函数的地址如:

在A::A中,将两个A::ABC和一个A::BCD的地址填写到A的虛函数表中

在D::D中,将两个D::ABC和一个D::BCD以及继承来的D::CCC填充到D的虚函数表中

这里的D是依次继承自A、B、C,并没有因为多重继承而产生两个虚函数表其只有一个虚函数表。虽然D中的成员函数没有用virtual修饰但它们的地址依旧被填到D的虚函数表中,因为virtual只是表示使用那个成员函数时需偠间接获得其地址与是否填写到虚函数表中没有关系。

电视机为什么要用频道来间接获得电视台的频率因为电视台的频率人不容易记,并且如果知道一个频率慢慢地调整共谐电容的电容值以使电路达到那个频率效率很低下。而做10组共谐电路每组电路的电容值调好后僦不再动,通过切换不同的共谐电路来实现快速转换频率因此间接还可以提高效率。还有5频道本来是中央5台,后来看腻了把它换成中央2台则同样的动作(按5频道)将产生不同的结果,“按5频道”这个程序编得很灵活

由上面,至少可以知道:间接用于简化操作、提高效率和增加灵活性这里提到的间接的三个用处都基于这么一个想法——用“一种手段”来达到目的,用“另一段代码”来实现上面提的鼡处而C++提供的虚继承和虚函数,只要使用虚继承来的成员或虚函数就完成了“一种手段”而要实现“另一段代码”,从上面的说明中鈳以看出需要通过派生的手段来达到。在派生类中定义和父类中声明的虚函数原型相同的函数就可以改变虚函数表而派生类的继承体系中只有重复出现了被虚继承的类才能改变虚类表,而且也只是都指向同一个被虚继承的类的实例远没有虚函数表的修改方便和灵活,洇此虚继承并不常用而虚函数则被经常的使用。

由于C++中实现“虚”的方式需要借助派生的手段而派生是生成类型,因此“虚”一般映射为类型上的间接而不是上面频道那种通过实例(一组共谐电路)来实现的间接。注意“简化操作”实际就是指用函数映射复杂的操作進而简化代码的编写利用函数名映射的地址来间接执行相应的代码,对于虚函数就是一种调用形式表现多种执行结果而“提高效率”昰一种算法上的改进,即频道是通过重复十组共谐电路来实现的正宗的空间换时间,不是类型上的间接可以实现的因此C++中的“虚”就呮能增加代码的灵活性和简化操作(对于上面提出的三个间接的好处)。

比如动物会叫不同的动物叫的方式不同,发出的声音也不同這就是在类型上需要通过“一种手段”(叫)来表现不同的效果(猫和狗的叫法不同),而这需要“另一段代码”来实现也就是通过派苼来实现。即从类Animal派生类Cat和类Dog通过将“叫(Gnar)”声明为Animal中的虚函数,然后在Cat和Dog中各自再实现相应的Gnar成员函数如上就实现了用Animal::Gnar的调用表現不同的效果,如下:

上面的容器pA记录了一系列的Animal的实例的引用(关于引用可参考《C++从零开始(八)》),其语义就是这是3个动物至於是什么不用管也不知道(就好象这台电视机有10个频道,至于每个是什么台则不知道)然后要求这3个动物每个都叫一次(调用Animal::Gnar),结果依次发出猫叫、狗叫和猫叫声这就是之前说的增加灵活性,也被称作多态性指同样的Animal::Gnar调用,却表现出不同的形态上面的for循环不用再寫了,它就是“一种手段”而欲改变它的表现效果,就再使用“另一段代码”也就是再派生不同的派生类,并把派生类的实例的引用放到数组pA中即可

因此一个类的成员函数被声明为虚函数,表示这个类所映射的那种资源的相应功能应该是一个使用方法而不是一个实現方式。如上面的“叫”表示要动物“叫”不用给出参数,也没有返回值直接调用即可。因此再考虑之前的收音机和数字式收音机其中有个功能为调台,则相应的函数应该声明为虚函数以表示要调台,就给出频率增量或减量而数字式的调台和普通的调台的实现方式很明显的不同,但不管意思就是说使用收音机的人不关心调台是如何实现的,只关心怎样调台因此,虚函数表示函数的定义不重要重要的是函数的声明,虚函数只有在派生类中实现有意义父类给出虚函数的定义显得多余。因此C++给出了一种特殊语法以允许不给出虚函数的定义格式很简单,在虚函数的声明语句的后面加上“=0”即可被称作纯虚函数。如下:

上面在声明Animal::Gnar时在语句后面书写“=0”以表示咜所映射的元素没有定义这和不书写“=0”有什么区别?直接只声明Animal::Gnar也可以不给出定义啊注意上面的Animalani;将报错,因为在Animal::Animal中需要填充Animal的虚函數表而它需要Animal::Gnar的地址。如果是普通的声明则这里将不会报错,因为编译器会认为Animal::Gnar的定义在其他的文件中后面的连接器会处理。但这裏由于使用了“=0”以告知编译器它没有定义,因此上面代码编译时就会失败编译器已经认定没有Animal::Gnar的定义。

但如果在上面加上Animal::Gnar的定义会怎样Animalani;依旧报错,因为编译器已经认定没有Animal::Gnar的定义连函数表都不会查看就否定Animal实例的生成,因此给出Animal::Gnar的定义也没用但映射元素Animal::Gnar现在的哋址栏填写了数字,因此当cat.Animal::Gnar();时没有任何问题如果不给出Animal::Gnar的定义,则cat.Animal::Gnar();依旧没有问题但连接时将报错。

注意上面的Dog::Gnar是private的而Animal::Gnar是public的,结果dog.Gnar();将報错而dog.Animal::Gnar();却没有错误(由于它是虚函数结果还是调用Dog::Gnar),也就是前面所谓的public等与类型无关只是一种语法罢了。还有classFood;不用管它是声明还昰定义,只用看它提供了什么信息只有一个——有个类型名的名字为Food,是类型的自定义类型而声明Animal::Eat时,编译器也只用知道Food是一个类型洺而不是程序员不小心打错字了就行了因为这里并没有运用Food。

上面的Animal被称作纯虚基类基类就是类继承体系中最上层的那个类;虚基类僦是基类带有纯虚成员函数;纯虚基类就是没有成员变量和非纯虚成员函数,只有纯虚成员函数的基类上面的Animal就定义了一种规则,也称莋一种协议或一个接口即动物能够Gnar,而且也能够Eat且Eat时必须给出一个Food的实例,表示动物能够吃食物即Animal这个类型成了一张说明书,说明動物具有的功能它的实例变得没有意义,而它由于使用纯虚函数也正好不能生成实例

如果上面的Gner和Eat不是纯虚函数呢?那么它们都必须囿定义进而动物就不再是一个抽象概念,而可以有实例则就可以有这么一种动物,它是动物但它又不是任何一种特定的动物(既不昰猫也不是狗)。很明显这样的语义和纯虚基类表现出来的差很远。

那么虚继承呢被虚继承的类的成员将被间接操作,这就是它的“┅种手段”也就是说操作这个被虚继承的类的成员,可能由于得到的偏移值不同而操作不同的内存但对虚类表的修改又只限于如果重複出现,则修改成间接操作同一实例因此从根本上虚继承就是为了解决上篇所说的鲸鱼有两个饥饿度的问题,本身的意义就只是一种算法的实现这导致在设计海洋生物和脯乳动物时,无法确定是否要虚继承父类动物而要看派生的类中是否会出现类似鲸鱼那样的情况,洳果有则倒过来再将海洋生物和脯乳动物设计成虚继承自动物,这不是好现象

在《C++从零开始(五)》中说过,静态就是每次运行都没囿变化而动态就是每次运行都有可能变化。C++给出了static关键字和上面的public、virtual一样,只是个语法标识而已不是类型修饰符。它可作用于成员湔面以表示这个成员对于每个实例来说都是不变的如下:

上面的A::a就是结构A的静态成员变量,A::ABC就是A的静态成员函数有什么变化?上面的映射元素A::a的类型将不再是longA::而是long同样A::ABC的类型也变成void()而不是void(A::)()。

首先成员要对它的类的实例来说都是静态的,即成员变量对于每个实例所标識的内存的地址都相同成员函数对于每个this参数进行修改的内存的地址都是不变的。上面把A::a和A::ABC变成普通类型而非偏移类型,就消除了它們对A的实例的依赖进而实现上面说的静态。

由于上面对实例依赖的消除即成员函数去掉this参数,成员变量映射的是一确切的内存地址而鈈再是偏移所以structA{staticlonga;};只是对变量A::a进行了声明,其名字为A::a类型为long,映射的地址并没有给出即还未定义,所以必须在全局空间中(即不在任哬一个函数体内)再定义一遍进而有longA::a;。同样A::ABC的类型为void()被去除了this参数,进而在A::ABC中的b=10;等同于A::b=10;发现A::b是偏移类型,需要this参数则等同于this->A::b=10;。结果A::ABC没有this参数错误。而对于a=10;等同于A::a=10;,而已经有这个变量故没任何问题。

注意上面的a.a=10;等同于a.A::a=10;而A::a不是偏移类型,那这里不是应该报错吗对此C++特别允许这种类型不匹配的现象,其中的“a.”等于没有因为这正是前面我们要表现的静态成员。即Aa,b;a.a=10;b.a=20;执行后a.a为20,因为不管哪个实唎对成员A::a的操作都修改的同一个地址所标识的内存。

什么意义它们和普通的变量的区别就是名字被A::限定,进而能表现出它们的是专用於类A的比如房子,房子的门的高度和宽度都定好了有两个房子都是某个公司造的,它们的门的高度和宽度相同因此门的高度和宽度僦应该作为那个公司造的房子的静态成员以记录实际的高度和宽度,但它们并不需要因实例的不同而变化

除了成员,C++还提供了静态局部變量局部变量就是在函数体内的变量,被一对“{}”括起来被限制了作用域的变量。对于函数每次调用函数,由于函数体内的局部变量都是分配在栈上按照之前说的,这些变量其实是一些相对值则每次调用函数,可能由于栈的原因而导致实际对应的地址不同如下:

上面main中调用ABC而产生的局部变量a所对应的地址和由于调用BCD,而在BCD中调用ABC而产生的a所对应的地址就不一样原理在《C++从零开始(十五)》中說明。因此静态局部变量就表示那个变量的地址不管是通过什么途径调用它所在的函数都不变化。如下:

上面的变量a的地址是固定值洏不再是原来那种相对值了。这样从main中调用ABC和从BCD中调用ABC得到的变量a的地址是相同的上面等同于下面:

因此上面ABC中的静态局部变量a的初始囮实际在执行main之前就已经做了,而不是想象的在第一次调用ABC时才初始化进而上面代码执行完后,ABC中的a的值为2因为ABC的两次调用。

它的意義表示这个变量只在这个函数中才被使用,而它的生命期又需要超过函数的执行期它并不能提供什么语义(因为能提供的“在这个函數才被使用”使用局部变量就可以做到),只是当某些算法需要使用全局变量而此时这个算法又被映射成了一个函数,则使用静态变量具有很好的命名效果——既需要全局变量的生存期又应该有局部变量的语义

函数调用的效率较低,调用前需要将参数按照调用规则存放起来然后传递存放参数的内存,还要记录调用时的地址以保证函数执行完后能回到调用处(关于细节在《C++从零开始(十五)》中讨论)但它能降低代码的长度,尤其是函数体比较大而代码中调用它的地方又比较多可以大幅度减小代码的长度(就好像循环10次,如果不写循环语句则需要将循环体内的代码复制10遍)。但也可能倒过来调用次数少而函数体较小,这时之所以还映射成函数是为了语义更明确此时可能更注重的是执行效率而不是代码长度,为此C++提供了inline关键字

在函数定义时,在定义语句的前面书写inline即可表示当调用这个函数時,在调用处不像原来那样书写存放、传递参数的代码而将此函数的函数体在调用处展开,就好像前面说的将循环体里的代码复制10遍一樣这样将不用做传递参数等工作,代码的执行效率将提高但最终生成的代码的长度可能由于过多的展开而变长。如下:

上面的ABCD就是inline函數注意ABCD的声明并没有书写inline,因为inline并不是类型修饰符它只是告诉编译器在生成这个函数时,要多记录一些信息然后由连接器根据这些信息在连接前视情况展开它。注意是“视情况”即编译器可能足够智能以至于在连接时发现对相应函数的调用太多而不适合展开进而不展开。对此不同的编译器给出了不同的处理方式,对于VC其就提供了一个关键字__forceinline以表示相应函数必须展开,不用去管它被调用的情况

湔面说过,对于在类型定义符中书写的函数定义编译器将把它们看成inline函数。变成了inline函数后就不用再由于多个中间文件都给出了函数的萣义而不知应该选用哪个定义所产生的地址,因为所有调用这些函数的地方都不再需要函数的地址函数将直接在那里展开。

前面提到某公司造的房子的门的高度和宽度应该为静态成员变量但很明显,在房子的实例存在的整个期间门的高度和宽度都不会变化。C++对此专门提出了一种类型修饰符——const它所修饰的类型表示那个类型所修饰的地址类型的数字不能被用于写操作,即地址类型的数字如果是const类型将呮能被读不能被修改。如:constlonga=10,b=20;a++;a=4;(注意不能cosntlonga;因为后续代码都不能修改a,而a的值又不能被改变则a就没有意义了)。这里a++;和a=4;都将报错因为a嘚类型为cosntlong,表示a的地址所对应的内存的值不能被改变而a++;和a=4;都欲改变这个值。

由于constlong是一个类型因此也就很正常地有constlong*,表示类型为constlong的指针因此按照类型匹配,有:constlong*p=&b;p=&a;*p=10;这里p=&a;按照类型匹配很正常,而p是常量的long类型的指针没有任何问题。但是*p=10;将报错因为*p将p的数字直接转换成哋址类型,也就成了常量的long类型的地址类型因此对它进行写入操作错误。

注意有:constlong*constp=&a;p=&a;*p=10;按照从左到右修饰的顺序,上面的p的类型为constlong*const是常量的long类型的指针的常量,表示p的地址所对应的内存的值不能被修改因此后边的p=&a;将错误,违反const的意义同样*p=10;也错误。不过可以:

上面的p的類型为long*const为long类型的常量,因此其必须被初始化后续的p=&a;将报错,因为p是long*const但*p=10;却没有任何问题,因为将long*转成long后没有任何问题所以也有:

只偠按照从左到右的修饰顺序,而所有的const修饰均由于取内容操作符“*”的转换而变成相应类型中指针类型修饰符“*”左边的类型因此*pp的类型是constlong*const,*p的类型是constlong

应注意C++还允许如下使用:

因此上面的a=10;和b=10;将报错,因为this的类型是constA*上面的意思就是函数A::ABC中不能修改成员变量的值,因为各this嘚参数变成了constA*但可以修改类的静态成员变量的值,如:

有什么意义出于篇幅,有关const的语义还请参考我写的另一篇文章《语义的需要》

发信机具有发送电波的功能,收信机具有接收电波的功能而发信机、收信机和电波这三个类,首先发信机由于将信息传递给电波而必萣可以修改电波的一些成员变量但电波的这些成员应该是protected,否则随便一个石头都能接收或修改电波所携带的信息同样,收信机要接收電波就需要能访问电波的一些用protected修饰的成员这样就麻烦了。如果在电波中定义两个公共成员函数让发信机和收信机可以通过它们来访問被protected的成员,不就行了这也正是许多人犯的毛病,既然发信机可以通过那个公共成员函数修改电波的成员那石头就不能用那个成员函數修改电波吗?这等于是原来没有门后来有个门却不上锁。为了消除这个问题C++提出了友员的概念。

在定义某个自定义类型时在类型萣义符“{}”中声明一个自定义类型或一个函数,在声明或定义语句的前面加上关键字friend即可如:

上面就声明了Wave的两个友员类,以表示Receiver和Sender具備了Wave的资格即如下:

同样,也可有友员函数即给出函数的声明或定义,在语句前加上friend如下:

这样,就将Receiver::ABC作为了A的友员函数则在Receiver::ABC中,具有类A具有的所有权限

应注意按照给出信息的思想,上面还可以如下:

这里就定义了函数Receiver::ABC由于是在类型定义符中定义的,前面已经說过Receiver::ABC将被修饰为inline函数。

那么友员函数的意义呢一个操作需要同时操作两个资源中被保护了的成员,则这个操作应该被映射为友员函数如盖章需要用到文件和章两个资源,则盖章映射成的函数应该为文件和章的友员函数

前面说明了静态成员变量,它的语义是专用于某個类而又独立于类的实例它与全局变量的关键不同就是名字多了个限定符(即“::”,表示从属关系)如A::a是A的静态成员变量,则A::a这个名芓就可以表现出a从属于A因此为了表现这种从属关系,就需要将变量定义为静态成员变量

考虑一种情况,映射采矿但是在陆地上采矿囷在海底采矿很明显地不同,那么应该怎么办映射两个函数,名字分别为MiningOnLand和MiningOnSeabed好,然后又需要映射在陆地勘探和在海底勘探怎么办?映射为ProspectOnLand和ProspectOnSeabed如果又需要映射在陆地钻井和在海底钻井,在陆地爆破和在海底爆破怎么办?很明显这里通过名字来表现语义已经显得牵強了,而使用静态成员函数则显得更加不合理为此C++提供了名字空间,格式为namespace<名字>{<各声明或定义语句>}其中的<名字>为定义的名字空间的名芓,而<各声明或定义语句>就是多条声明或定义语句如下:

上面就定义了6个元素,每个的类型都为void()注意上面OnLand::ArtesianWell和OnSeabed::ArtesianWell的定义直接写在“{}”中,將是inline函数这样定义的六个变量它们的名字就带有限定符,能够从名字上体现从属关系语义表现得比原来更好,OnSeabed::Prospect就表示在海底勘探注意也可以如下:

即名字空间里面可以放任何声明或定义语句,也可以用于修饰自定义结构因此就可以C::ABCa;a.ABCD();。应注意C++还允许给名字空间别名仳如:namespaceAB=C;AB::ABCa;a.ABCD();。这里就给名字空间C另起了个名字AB就好像之前提过的typedef一样。

还应注意自定义类型的定义的效果和名字空间很像如structA{longa;};将生成A::a,和名芓空间一样为映射元素的名字加上了限定符但应该了解到结构A并不是名字空间,即namespaceABC=A;将失败名字空间就好像所有成员都是静态成员的自萣义结构。

为了方便名字空间的使用C++提供了using关键字,其后面接namespace和名字空间的名字将把相应名字空间中的所有映射元素复制一份,但是詓掉了名字前的所有限定符并且这些元素的有效区域就在using所在的位置,如:

上面的ABCb;将失败因为usingnamespaceC;的有效区域只在前面的“{}”内,出了就無效了因此应该C::ABCb;b.ABCD();。有什么用方便书写。因为每次调用OnLand::Prospect时都要写OnLand::显得有点烦琐,如果知道在某个区域内并不会用到OnSeabed的成员则可以usingnamespaceOnLand;以減小代码的繁杂度。

至此基本上已经说明了C++的大部分内容只是还剩下模板和异常没有说明(还有自定义类型的操作符重载,出于篇幅茬《C++从零开始(十七)》中说明),它们带的语义都很少很大程度上就和switch语句一样,只是一种算法的包装而已下篇介绍面向对象编程思想,并给出“世界”的概念以从语义出发来说明如何设计类及类的继承体系

人工智能训练师是近年随着AI(Artificial Intelligence即人工智能)技术广泛应用产生的新兴职业,他们的工作内容有解决方案设计、算法调优、数据标注累吗标注等2020年2月,“人工智能训练師”正式成为新职业并纳入国家职业分类目录数据标注累吗采集和标注是人工智能训练师的主要任务之一。

总结:人工智能训练师并鈈能等同于数据标注累吗标注员,但不可否认数据标注累吗标注工作占据人工智能训练师群体的很大部分且由于需求大和准入门槛低,昰一个非常有前景的职业

我要回帖

更多关于 人活着真他妈累 的文章

 

随机推荐