标签:vector www div 定义函数 决定 virt binary 不同的 fonts
转自: https://www.cnblogs.com/llguanli/p/8732481.html
本章目的:
当Android用ART虚拟机替代Dalvik的时候,为了表示和Dalvik彻底划清界限的决心,Google连ART虚拟机的实现代码都切换到了C++11。C+11的标准规范于2011年2月正式落稿。而此前10余年间,C++正式标准一直是C++98/03[①]。相比C++98/03。C++11有了非常多的变化,甚至一度让笔者大呼不认识C++了[②]。
只是。作为科技行业的从业者,我们要铭记在心的一个铁规就是要拥抱变化。
既然我们不认识C++11。那就把它当做一门全新的语言来学习吧。
从2007年到2010年,在我參加工作的头三年中,笔者一直使用C++作为唯一的开发语言,写过十几万行的代码。从2010年转向Android开发后,我才正式接触Java。此后非常多年里,我曾经多次比較过两种语言,有了一些非常直观,非常感性的看法。此处和大家分享,读者最好还是一看:
对于业务系统[③]的开发而言,Java相比C++而言,开发确实方便太多。比方:
而C++开发则高度依赖于操作系统以及硬件平台。比方Windows的C++程序到Linux平台上差点儿都无法直接使用。这当中的问题倒也不能全赖在C++语言本身上。
仅仅是选择一门开发语言不仅仅是选择语言本身,其背后的生态系统(OS。硬件平台,公共类库,开发资源,文档等)随之也被选择。
比方,当年我在Windows搞一套C++封装的多线程工具类。之后移植到Linux上又得搞一套。而且还要花非常多精力维护它们。
个人感受:
我个人对C++是没有不论什么偏好的。
之所以用C++,非常大程度上是由于直接领导的选择。作为一个工作多年的老员工。在他印象里,那个年代的Java性能非常差。比不得C++的机灵和高效。另外,由于我们做得是高性能视音频数据网络传输(在局域网/广域网,几个GB的视音频文件相似FTP这样的上传下载),C++貌似是当时唯一能同一时候和“面向对象”。“性能不错”挂上钩的语言了。
在研究ART的时候,笔者发现其源代码是用一种和我曾经熟悉得C++差别非常大的C++语言编写得。这样的差别甚至一度让我感叹“不太认识C++语言了”。
后来,我才了解到这样的“全新的”C++就是C++11。当时我就在想,包括我自己在内,以及本书的读者们要不要学习它呢?思来覆去,我认为还是有这个必要:
既然下定决心,那么就立即開始学习。正式介绍C++11前。笔者要特别强调以下几点注意事项:
所以本章所介绍的C++11内容,一切以看懂ART源代码为最高目标。
源代码中没有涉及的C++11知识。本章尽量不予介绍。一些细枝末节,或者高深精尖的使用方法。笔者也不拟详述。假设读者想深入研究,最好还是阅读本章參考文献所列出的六本C++专著。
用C++敲代码,会碰到非常多所谓的“坑”。仅仅有亲历并吃过亏之后,才干深刻掌握这门语言。所以,假设读者想真正学好C++。那么一定要多写代码。不能停留在看懂代码的水平上。
注意:
最后。本章不是专门来讨论C++语法的,它更大的作用在于帮助读者更快得了解C++。
故笔者会尝试採用一些通俗的语言来介绍它。因此,本章在关于C++语法描写叙述的精准性上必定会有所不足。在此。笔者一方面请读者谅解,还有一方面请读者及时反馈所发现的问题。
以下。笔者将正式介绍C++11,本章拟解说例如以下内容:
学习一门语言。首先从它定义的数据类型開始。
本节先介绍C++基本内置的数据类型。
图1所看到的为C++中的基本内置数据类型(注意,图中没有包括全部的内置数据类型):
图1 C++基本数据类型
图1展示了C++语言中几种经常使用的基本数据类型。有几点请读者注意:
对于双精度浮点数double类型而言。要求最少支持10个有效数字。
注意:
本章中,笔者可能会经常拿Java语言做对照。由于了解语言之间的差异更有助于高速掌握一门新的语言。
和Java不同的是,C++中的数据类型分无符号和有符号两种,比方:
图2 无符号数据类型定义
注意,无符号类型的关键词为unsigned。
如今来看C++里另外三种经常使用的数据类型:指针、引用和void。如图3所看到的:
图3 指针、引用和void
由图3可知:
这样的类型仅仅能用于定义指针变量。比方void*。当我们确实不关注内存中存储的数据究竟是什么类型的话。就能够定义一个void*类型的指针来指向这块内存。
以下我们着重介绍一下指针和引用。先来看指针:
关于指针。读者仅仅须要掌握三个基本知识点就能够了:
指针本质上代表了虚拟内存的地址。简单点说。指针就是内存地址。比方,在32位系统上。一个进程的虚拟地址空间为4G,虚拟内存地址从0x0到0xFFFFFFFF,这个段中的不论什么一个值都是内存地址。
一个程序运行时。其虚拟内存中会有什么呢?肯定有数据和代码。假设某个指针指向一块内存,该内存存储的是数据,C++中数据都得有数据类型。所以,指向这块内存的指针也应该有类型。
比方:
2 int* p。变量p是一个指针,它指向的内存存储了一个(对于数组而言,就是一组)int型数据。
2 short* p,变量p指向的内存存储了一个(或一组)short型数据。
假设指针相应的内存中存储的是代码的话,那么指向这块代码入口地址(代码往往是封装在函数里的,代码的入口就是函数的入口)的指针就叫函数指针。
函数指针的定义看起来有些古怪,如图4所看到的:
图4 函数指针定义演示样例
提示:
函数指针的定义语法看起来比較奇特。笔者也是实践了非常多次才了解它。
定义指针变量后,下一个要考虑的问题就是给它赋什么值。
来看图5:
图5 指针变量的赋值
结合图5可知,指针变量的赋值有几种形式:
注意
函数指针变量的赋值也能够直接使用目标函数名。也可使用取地址符&。二者效果一致
指针仅仅是代表内存的某个地址,怎样获取该地址相应内存中的内容呢?C++提供了解指针引用符号*来帮助大家。如图6所看到的:
图6 指针解引用
图6中:
讨论:
为什么C/C++中会有指针呢?由于C和C++语言作为系统编程(System Programming)语言,出于运行效率的考虑。它提供了指针这样的机制让程序猿能够直接操作内存。当然,这样的做法的利弊已经讨论了几十年,其主要坏处就在于大部分程序猿管不好内存。导致经常出现内存泄露,訪问异常内存地址等各种问题。
相比C。引用是C++特有的一个概念。我们来看图7,它展示了指针和引用的差别:
图7 引用的使用方法演示样例(1)
图7 引用的使用方法演示样例(2)
由图7可知:
C语言中没有引用。一样工作得非常好。那么C++引入引用的目的是什么呢[⑤]?
和Java比較
和Java语言比起来,假设Java中函数的形參是基础类型(如int,long之类的)。则这个形參是传值的,与图7中的changeNoRef相似。假设这个函数的形參是类类型。则该形參相似于图7中的changeRef。在函数内部改动形參的数据,实參的数据相应会被改动。
图8所看到的为字符和字符串的演示样例:
图8 字符和字符串演示样例
请读者注意图8中的Raw字符串定义的格式。它的标准格式为R"附加界定符(字符串)附加界定符"。附加界定符能够没有。
而笔者设置图8中的附加界定符为"**123"。
Raw字符串是C++11引入的,它是为了解决正則表達式里那些烦人的转义字符\而提供的解决方法。来看看C++之父给出的一个样例,有这样一个正則表達式(‘(?:[?\\‘]|\\.)?‘|"(?:[?\\"]|\\.)?")|)
使用转义字符后,整个字符串变得非常难看懂了。
:[?\\‘]|\\.)?‘|"(?:[?\\"]|\\.)?")|)dfp"就可以。此处使用的界定字符为"dfp"。
非常显然。使用Raw字符串使得代码看起来更清爽,出错的可能性也降低非常多。
直接来看关于数组的一个演示样例。如图9所看到的:
图9 数组演示样例
由图9可知:
数组大小能够在编译时由初值列表的个数决定,也能够是一个常量。总之,这样的类型的数组。其数组大小必须在编译时决定。
程序中,代表动态数组的是一个相应类型的指针变量。
所以,动态数组和指针变量有着天然的关系。
和Java比較
Java中,数组的定义方式是T[]name。笔者认为这样的书写方式比C++的书写方式要形象一些。
另外。Java中的数组都是动态数组。
了解完数据类型后,我们来看看C++中源代码构成及编译相关的知识。
源代码构成是指怎样组织、管理和编译源代码文件。作为对照,我们先来看Java是怎么处理的:
所以class A的全路径名为xx.yy.zz.A。当中,xx.yy.zz是包名。
综其所述,源代码构成主要讨论两个问题:
不同package下,则必须通过全路径名訪问另外一个Package下的源文件A的内容(通过import能够降低书写包名的工作量)。
如今来看C++的做法:
也能够.hpp,.hxx结尾。源文件以.cpp,.cxx和.cc结尾。
仅仅要开发人员之间约定好,採用什么形式的后缀都能够。笔者个人喜欢使用.h和.cpp做后缀名。而art源代码则以.h和.cc为后缀名。
以下我们分别通过头文件和源文件的几个演示样例来强化对它们的认识。
图10所看到的为一个非常easy头文件演示样例:
图10 Type.h演示样例
以下来分析图10中的Type.h:
这三个宏合起来的意思是,假设未定义_TYPE_H_,则定义它。
宏的名字能够随意取,但通常是和头文件的文件名称相关。而且该宏不要和其它宏重名。
为什么要定义一个这样的宏呢?其目的是为了防止头文件的反复包括。
探讨:怎样防止头文件反复包括
编译器处理#include命令的方式就是将被包括的头文件的内容全部读取进来。
一般而言,这样的包括关系非常复杂。比方,a.h能够直接包括b.h和c.h,而b.h也能够直接包括c.h。
如此,a.h相当于直接包括c.h一次,并间接包括c.h(通过b.包括c.h的方式)一次。假设c.h採用和图10一样的做法,则编译器在第一次包括c.h(由于a.h直接#include"c.h")的时候将定义_C_H_宏。当编译器第二次尝试包括c.h的时候(由于在处理#include "b.h"的时候。会将b.h所include的文件依次包括进来)会发现这个宏已经定义了。由于头文件里全部有价值的内容都是写在#ifndef和#endif之间的,也就是仅仅有在未定义_C_H_宏的时候,这个头文件的内容才会真正被包括进去。通过这样的方式,c.h尽管被include两次,可是仅仅有第一次包括会载入其内容。兴许include等于没有真正载入其内容。
当然。如今的编译器比較高级。也许能够处理这样的反复包括头文件的问题,可是建议读者自己写头文件的时候还是要定义这样的宏。
除了宏定义之外,图10中还定义了一个命名空间。名字为my_type。而且在命名空间里还声明了一个test函数:
凡是放在某个命名空间里的函数,类,变量等就属于这个命名空间。
以下我们来看一个源文件演示样例:
源文件演示样例一如图11所看到的:
图11 Test.cpp演示样例
图11是一个名为Test.cpp的演示样例。在这个演示样例中:
由于test位于my_type命名空间里,所以须要通过my_type::test方式来调用它。
接着来看图12:
图12 Type.cpp
图12所看到的为Type.cpp:
出于文件管理方便性的考虑。头文件和相应的源文件有着同样的文件名称。
比方,#include <iostream>这条语句无需写成#include <iostream.h>。
这是由于C++标准库的实现是由不同厂商来完毕的。
详细实现的时候可能头文件没有后缀名。或者后缀名不是.h。
所以,C++规范将这个问题交给编译器来处理。它会依据情况找到正确的文件。
当然,也能够一次性将某个命名空间里的全部内容全部包括进来,方法就是usingnamespace std。这样的做法和java的import非常相似。
而由于changeRef全然是在Type.cpp中定义的,所以仅仅有Type.cpp内部才知道这个函数,而外界(其它源文件,头文件)不知道这个世界上还有一个changeRef函数。在此请读者注意,一般而言,include指令用于包括头文件。极少用于包括源文件。
到此,我们通过几个演示样例向读者展示了C++中头文件和源文件的构成和一些经常使用的代码写法。如今看看怎样编译它们。
C/C++程序通常是通过编写Makefile来编译的。
Makefile事实上就是一个命令的组合,它会依据情况运行不同的命令,包括编译。链接等。Makefile不是C++学习的必备知识点,笔者不拟讨论太多,读者通过图13做简单了解就可以:
图13 Makefile演示样例
图13中,真正的编译工作还是由编译器来完毕的。图13中展示了编译器的工作步骤以及相应的參数。此处笔者仅强调三点:
其内容的书写规则遵守make命令的要求。
make命令怎样运行呢?非常easy:
make将解析Makefile文件里定义的任务以及它们的依赖关系。然后对任务进行处理。假设没有指明任务名的话,则运行Makefile中定义的第一个任务。
提示
Makefile和make是一个独立的知识点,关于它们的故事能够写出一整本书了。
只是,就实际工作而言。开发人员往往会把Makefile写好。或者可借助一些工具以自己主动生成Makefile。
所以。假设读者不了解Makefile的话也不用操心,仅仅要会运行make命令就能够了。
本节介绍C++中面向对象的核心知识点——类(Class)。笔者对类有三点认识:
程序总还是有顺序。有流程的。
可是在这个流程里,开发人员很多其它关注的是对象以及对象之间的交互,而不是孤零零的函数。
探讨:
笔者曾经差点儿没有从类型的角度来看待过类。
直到接触模板编程后,才发现类型和类型推导在模板中的重要作用。关于这个问题,我们留待兴许介绍模板编程时再继续讨论。
以下我们来看看C++中的Class该怎么实现。先来看图14所看到的的TypeClass.h。它声明了一个名为Base的类。请读者重点关注它的语法:
图14 Base类的声明
来看图14的内容:
Java中,每个成员(包括函数和变量)都须要单独声明訪问权限,而C++则是分组控制的。比如,位于"public:"之后的成员都有同样的public訪问权限。假设没有指明訪问权限,则默认使用private訪问权限。
接下来,我们先介绍C++的三大类特殊函数。
注意。
这三类特殊函数并非都须要定义。笔者此处列举它们仅为学习用。
C++类的三种特殊成员函数各自是构造、赋值和析构。当中:
Java中,和析构函数相似的是finalize方法。只是,由于Java实现了内存自己主动回收机制。所以Java程序猿差点儿不须要考虑finalize的事情。
以下,我们分别来讨论这三种特殊函数。
来看类Base的构造函数,如图15所看到的:
图15 构造函数演示样例
图15中的代码实现于TypeClass.cpp中:
以下来介绍图15中几个值得注意的知识点:
构造函数基本的功能是完毕类实例的初始化。也就是对象的成员变量的初始化。C++中。成员变量的初始化推荐使用初始值列表(constructor initialize list)的方法(使用方法如图15所看到的),其语法格式为:
构造函数(...):
成员变量A(A的初值),成员变量B(B的初值){
...//也能够使用花括号,比方成员变量A{A的初值},成员变量B{B的初值}
}
当然,成员变量的初值设置也能够通过赋值方式来完毕:
构造函数(...){
成员变量A=A的初值;
成员变量B=B的初值;
....
}
C++中。构造函数中使用初值列表和成员变量赋初值是有差别的,此处不拟详细讨论二者的差异。但推荐使用初值列表的方式,原因大致有二:
提示:
构造函数中请使用初值列表的方式来完毕变量初始化。
拷贝构造,即从一个已有的对象拷贝其内容,然后构造出一个新的对象。拷贝构造函数的写法必须是:
构造函数(const 类& other)
注意,const是C++中的常量修饰符,与Java的final相似。
拷贝过程中有一个问题须要程序猿特别注意,即成员变量的拷贝方式是值拷贝还是内容拷贝。以Base类的拷贝构造为例。假设新创建的对象名为B,它用已有的对象A进行拷贝构造:
所以,A对象的memberA和memberB将赋给B的memberA和memberB。此后,A、B对象的memberA和memberB值分别同样。
值拷贝、内容拷贝和浅拷贝、深拷贝
由上述内容可知,浅拷贝相应于值拷贝,而深拷贝相应于内容拷贝。对于非指针变量类型而言。值拷贝和内容拷贝没有差别,但对于指针型变量而言,值拷贝和内容拷贝差别就非常大了。
图16解释了深拷贝和浅拷贝的差别:
图16 浅拷贝和深拷贝的差别
图16中,浅拷贝用红色箭头表示。深拷贝用紫色箭头表示:
最后,笔者还要特别说明拷贝构造函数被触发的场合。
来看代码:
Base A; //构造A对象
Base B(A);// ①直接用A对象来构造B对象,这样的情况是“直接初始化”
Base C = A;// ②定义C的时候即赋值。这是真正意义上的拷贝构造。二者的差别见下文介绍。
除了上述两种情况外,还有一些场合也会导致拷贝构造函数被调用,比方:
直接初始化和拷贝初始化的细微差别
Base B(A)仅仅是导致拷贝构造函数被调用,但并非严格意义上的拷贝构造,由于:
拷贝赋值函数是赋值函数的一种。我们先来思考下赋值函数解决什么问题。请读者思考以下这段代码:
int a = 0;
int b = a;//将a赋值给b
全部读者应该对上述代码都不会有不论什么疑问。是的,对于基本内置数据类型而言,赋值操作似乎是天经地义的合理。但对于类类型呢?比方以下的代码:
Base A;//构造一个对象A
Base B; //构造一个对象B
B = A; //①A能够赋值给B吗?
从类型的角度来看。没有理由不同意类这样的自己定义数据类型的进行赋值操作。
可是从面向对象角度来看,把一个对象赋值给另外一个对象会得到什么?现实生活中似乎也难以到相似的场景来比拟它。
无论怎样,C++是支持一个对象赋值给还有一个对象的。
如今把注意力回归到拷贝赋值上来。来看图17所看到的的代码:
图17 拷贝赋值函数演示样例
赋值函数本身没有什么难度。无非就是在准备接受另外一个对象的内容前。先把自己清理干净。另外,赋值函数的关键知识点是利用了C++中的操作符重载(Java不支持操作符重载)。关于操作符重载的知识请读者阅读本文兴许章节。
前面两节介绍了拷贝构造和拷贝赋值函数,还了解了深拷贝和浅拷贝的差别。
但关于构造和赋值的故事并没有完。由于C++11中,除了拷贝构造和拷贝赋值之外,还有移动构造和移动赋值。
注意
这几个名词中:构造和赋值并没有变。变化的是构造和赋值的方法。前2节介绍的是拷贝之法,本节来看移动之法。
图18展示了移动的含义:
图18 Move的示意
对照图16和图18,读者会发现移动的含义事实上非常easy。就是把A对象的内容移动到B对象中去:
假设使用拷贝之法。A和B对象将各自有一块内存。
假设使用移动之法,A对象将不再拥有这块内存。反而是B对象拥有A对象之前拥有的那块内存。
移动的含义好像不是非常难。只是,让我们更进一步思考一个问题:移动之后,A、B对象的命运会发生怎样的改变?
注意,A对象还存在。可是你最好不要碰它,由于它的内容早已经移交给了B。
移动之后。A竟然无用了。什么场合会须要如此“残忍”的做法?还是让我们用演示样例来阐述C++11推出移动之法的目的吧:
图19 有Move和没有Move的差别
图19中,左上角是演示样例代码:
图19展示了未定义移动构造函数和定义了移动构造函数时该程序运行后打印的日志。同一时候图中还解释了运行的过程。结合前文所述内容,我们发现tmp确实是一种转移出去(无论是採用移动还是拷贝)后就不须要再使用的对象了。对于这样的情况,移动构造所带来的优点是显而易见的。
注意:
对于图中的測试函数,如今的编译器已经能做到高度优化,以至于图中列出的移动或拷贝调用都不须要了。
为了达到图中的效果,编译时必须加上-fno-elide-constructors标志以禁止这样的优化。读者最好还是一试。
以下,我们来看看代码中是怎样体现移动的。
图20所看到的为Base的移动构造和移动赋值函数:
图20 移动构造和移动赋值演示样例
图20中,请读者特别注意Base类移动构造和移动赋值函数的參数的类型,它是Base&&。没错,是两个&&符号:
什么是左值,什么是右值?笔者不拟讨论它们详细的语法和语义。
只是。依据參考文献[5]所述,读者掌握例如以下识就可以:
比方图19中getTemporyBase返回的那个暂时对象就是无名的。它就是右值。
我们通过几行代码来加深对左右值的认识:
int a,b,c; //a,b,c都是左值
c = a+b; //c是左值,可是(a+b)却是右值,由于&(a+b)取地址不合法
getTemporyBase();//返回的是一个无名的暂时对象,所以是右值
Base && x = getTemoryBase();//通过定义一个右值引用类型x。getTemporyBase函数返回
//的这个暂时无名对象从此有了x这个名字。只是。x还是右值吗?答案为否:
Base y = x;//此处不会调用移动构造函数。而是拷贝构造函数。由于x是有名的。所以它不再是右值。
假设读者想了解很多其它关于左右值的差别,请阅读本章所列的參考书籍。此处笔者再强调一下移动构造和赋值函数在什么场合下使用的问题,请读者注意把握两个关键点:
为此。我们须要强制使用移动构造函数,方法为Base y = std::move(x)。move是std标准库提供的函数,用于将參数类型强制转换为相应的右值类型。通过move函数。我们表达了强制使用移动函数的想法。
假设未定义移动函数怎么办?
假设类未定义移动构造或移动赋值函数,编译器会调用相应的拷贝构造或拷贝赋值函数。所以,使用std::move不会带来什么副作用。它仅仅是表达了要使用移动之法的愿望。
最后,来看类中最后一类特殊函数。即析构函数。
当类的实例达到生命终点时,析构函数将被调用,其主要目的是为了清理该实例占领的资源。
图21所看到的为Base类的析构函数演示样例:
图21 析构函数演示样例
Java中与析构函数相似的是finalize函数。
但绝大多数情况下。Java程序猿不用关心它。而C++中,我们须要知道析构函数什么时候会被调用:
2 栈上创建的类实例。在退出作用域(比方函数返回,或者离开花括号包围起来的某个作用域)之前,该实例会被析构。
2 动态创建的实例(通过new操作符)。当delete该对象时,其析构函数会被调用。
1.3.1节介绍了C++中一个普通类的大致组成元素和当中一些特殊的成员函数,比方:
请读者先从原理上理解拷贝和移动的差别和它们的目的。
C++中与类的派生、继承相关的知识比較复杂,相对琐碎。本节中,笔者拟将精力放在一些相对基础的内容上。
先来看一个派生和继承的样例。如图22所看到的:
图22 派生和继承演示样例
图22中:
和Java比較
Java中尽管没有类的多重继承。但一个类能够实现多个接口(Interface),这事实上也算是多重继承了。
相比Java的这样的设计,笔者认为C++中类的多重继承太过灵活。使用时须要特别小心,否则菱形继承的问题非常难避免。
如今,先来看一下C++中派生类的写法。如图22所看到的,Derived类继承关系的语法例如以下:
class Derived:private Base,publicVirtualBase{
}
当中:
了解C++中怎样编写派生类后,下一步要关注面向对象中两个重要特性——多态和抽象是怎样在C++中体现的。
注意:
笔者此处所说的抽象是狭义的。和语言相关的,比方Java中的抽象类。
Java语言里,多态是借助派生类重写(override)基类的函数来表达,而抽象则是借助抽象类(包括抽象方法)或者接口来实现。
而在C++中,虚函数和纯虚函数就是用于描写叙述多态和抽象的利器:
从这一点看,它和Java的抽象类和接口非常相似。
C++中,虚函数和纯虚函数须要明白标示出来。以VirtualBase为例,相关语法例如以下:
virtual voidtest1(bool test); //虚函数由virtual标示
virtual voidtest2(int x, int y) = 0;//纯虚函数由"virtual"和"=0"同一时候标示
派生类怎样override这些虚函数呢?来看Derived类的写法:
/*
基类里定义的虚函数在派生类中也是虚函数,所以。以下语句中的virtual关键词不是必须要写的,
override关键词是C++11新引入的标识,和Java中的@Override相似。
override也不是必须要写的关键词。但加上它后,编译器将做一些实用的检查,所以建议开发人员
在派生类中重写基类虚函数时都加上这个关键词
*/
virtual void test1(bool test) override;//能够加virtual关键词,也能够不加
void test2(int x, int y) override;//如上,建议加上override标识
注意,virtual和override标示仅仅在类中声明函数时须要。
假设在类外实现该函数。则并不须要这些关键词,比方:
TypeClass.h
class Derived ....{
.......
voidtest2(int x, int y) override;//能够不加virtualkeyword
}
TypeClass.cpp
void Derived::test2(int x, int y){//类外定义这个函数。不能加virtual等关键词
cout<<"in Derived::test2"<<endl;
}
提示:
注意。art代码中,派生类override基类虚函数时,大都会加入virtual关键词,有时候也会加上override关键词。依据參考文献[1]的建议,派生类重写虚函数时候最好加入override标识。这样编译器能做一些额外检查而能提前发现一些错误。
除了上述两类虚函数外。C++中还有虚析构函数。
虚析构函数事实上就是虚函数。只是它略微有一点特殊。须要开发人员注意:
但对析构函数而言,由于析构函数的函数名必须是"~类名"。所以派生类和基类的析构函数名肯定是不同的。
比方,当通过基类指针来删除派生类对象时。是派生类对象的析构函数被调用。所以,当基类中假设有虚函数时候。一定要记得将其析构函数变成虚析构函数。
阻止虚函数被override
C++中,也能够阻止某个虚函数被override。方法和Java相似,就是在函数声明后加入final关键词。比方
virtual void test1(boolean test) final;//如此,test1将不能被派生类override了
最后,我们通过一段演示样例代码来加深对虚函数的认识。如图23所看到的:
图23 虚函数測试演示样例
图23是笔者编写的一个非常easy的样例。左边是代码,右边是运行结果。简而言之:
仅仅有这样,指向派生类对象的基类指针变量被delete时,派生类的析构函数才干被调用。
提示:
1 请读者尝试改动測试代码,然后观察打印结果。
2 读者可将图23中代码的最后一行改写成pvb->~VirtualBase(),即直接调用基类的析构函数,但由于它是虚析构函数,所以运行时。~Derived()将先被调用。
类的构造函数在类实例被创建时调用,而析构函数在该实例被销毁时调用。假设该类有派生关系的话。其基类的构造函数和析构函数也将被依次调用到,那么,这个依次的顺序是什么?
所以Base的构造函数先于VirtualBase调用,最后才是Derived的构造函数。
假设是多重继承的话,基类依照它们在派生列表里出现的相反次序调用各自的析构函数。
比方Derived类实例析构时。Derived析构函数先调用,然后VirtualBase析构,最后才是Base的析构。
补充内容:
假设派生类含有类类型的成员变量时,调用次序将变成:
构造函数:基类构造->派生类中类类型成员变量构造->派生类构造
析构函数:派生类析构->派生类中类类型成员变量析构->基类析构
多重派生的话,基类依照派生列表的顺序/反序构造或析构
Java中。假设程序猿没有为类编写构造函数函数。则编译器会为类隐式创建一个不带不论什么參数的构造函数。这样的编译器隐式创建一些函数的行为在C++中也存在。仅仅只是C++中的类有构造函数,赋值函数,析构函数,所以情况会复杂一些,图24描写叙述了编译器合成特殊函数的规则:
图24 编译器合成特殊函数的规则
图24的规矩可简单总结为:
从上面的描写叙述可知,C++中编译器合成特殊函数的规则是比較复杂的。即使如此。图24中展示的规则还仅是冰山一角。以移动函数的合成而言。即使图中的条件满足。编译器也未必能合成移动函数,比方类中有无法移动的成员变量时。
关于编译器合成规则,笔者个人感觉开发人员应该以实际需求为出发点,假设确实须要移动函数,则在类声明中定义就可以。
有些时候我们须要一种方法来控制编译器这样的自己主动合成的行为,控制的目的无外乎两个:
借助=default和=delete标识,这两个目的非常easy达到,来看一段代码:
//定义了一个普通的构造函数。但同一时候也想让编译器合成默认的构造函数。则能够使用=default标识
Base(int x); //定义一个普通构造函数后,编译器将停止自己主动合成默认的构造函数
//=default后。强制编译器合成默认的构造函数。注意。开发人员不用实现该函数
Base() = default;//通知编译器来合成这个默认的构造函数
//假设不想让编译器合成某些函数,则使用= delete标识
Base&operator=(const Base& other) = delete;//阻止编译合成拷贝赋值函数
注意,这样的控制行为仅仅针对于构造、赋值和析构等三类特殊的函数。
一般而言,派生类可能希望有着和基类相似的构造方法。比方。图25所看到的的Base类有3种普通构造方法。如今我们希望Derived也能支持通过这三种方式来创建Derived类实例。怎么办?图25展示了两种方法:
图25 派生类“继承”基类构造函数
继承之后。编译器会自己主动合成相应的构造函数。
注意,这样的“继承”事实上是一种编译器自己主动合成的规则。它仅支持合成普通的构造函数。而默认构造函数,移动构造函数。拷贝构造函数等遵循正常的规则来合成。
探讨
前述内容中。我们向读者展示了C++中编译器合成一些特殊函数的做法和规则。实际上,编译器合成的规则比本节所述内容要复杂得多。建议感兴趣的读者阅读參考文献来开展进一步的学习。
另外,实际使用过程中,开发人员不能全然依赖于编译器的自己主动合成。有些细节问题必须由开发人员自己先回答。比方。拷贝构造时,我们须要深拷贝还是浅拷贝?需不须要支持移动操作?在获得这些问题答案的基础上,读者再结合编译器合成的规则,然后才选择由编译器来合成这些函数还是由开发人员自己来编写它们。
前面我们提到过,C++中的类訪问事实上例的成员变量或成员函数的权限控制上有着和Java相似的关键词,如public、private和protected。严格遵守“信息该公开的要公开,不该公开的一定不公开”这一封装的最高原则无疑是一件好事。但现实生活中的情况是如此变化万端,有时候我们也须要破个例。
比方。熟人之间能否够公开一些信息以避开假设按“公事公办”走流程所带来的过高沟通成本的问题?
C++中,借助友元,我们能够做到小范围的公开信息以降低沟通成本。从编程角度来看,友元的作用无非是:提供一种方式,使得类外某些函数或者某些类能够訪问一个类的私有成员变量或成员函数。
对被訪问的类而言,这些类外函数或类,就是被訪问的类的朋友。
来看友元的演示样例。如图26所看到的:
图26 类的友元示意
图26展示了怎样为某个类指定它的“朋友们”,C++中,类的友元能够是:
假设友元是函数。则必须指定该函数的完整信息,包括返回值,參数,属于哪个类等。
基类的友元会变成从该基类派生得来的派生类的友元吗?
C++中,友元关系不能继承,也就是说:
1 基类的友元能够訪问基类非公开成员。也能訪问派生类中属于基类的非公开成员。
2 可是不能訪问派生类自己定义的非公开成员。
友元比較简单,此处就不拟多说。如今我们介绍下图26中提到的类的前向声明,先来回想下代码:
class Obj;//类的前向声明
void accessObj(Obj& obj);
C++中。数据类型应该先声明,然后再使用。
但这会带来一个“先有鸡还是先有蛋”的问题:
可是类Obj的声明却放在图26的最后。
怎么破解这个问题?这就用到了类的前向声明,以图26为例,Obj前向声明的目的就是告诉类型系统。Obj是一个class。不要把它当做别的什么东西。
一般而言,类的前向声明的使用方法例如以下:
可是我们不想在b.h里包括a.h。由于a.h可能太复杂了。
假设b.h里包括a.h,那么全部包括b.h的地方都间接包括了a.h。此时。通过引入A的前向声明,b.h中能够使用类A。
比方,b.cpp(b.h相相应的源文件)是真正使用该前向声明类的地方。那么仅仅要在b.cpp里包括a.h就可以。
这就是类的前向声明的使用方法,即在头文件里进行类的前向声明,在源文件里去包括该类的头文件。
类的前向声明的局限
前向声明优点非常多,但同一时候也有限制。以Obj为例,在看到Obj完整定义之前,不能声明Obj类型的变量(包括类的成员变量),可是能够定义Obj引用类型或Obj指针类型的变量。比方,你无法在图26中class Obj类代码之前定义ObjaObj这样的变量。
仅仅能定义Obj& refObj或Obj* pObj。之所以有这个限制,是由于定义Obj类型变量的时候,编译器必须确定该变量的大小以分配内存,由于没有见到Obj的完整定义。所以编译器无法确定其大小,但引用或者指针则不存在此问题。
读者最好还是一试。
explicit构造函数和类型的隐式转换有关。
什么是类型的隐式转换呢?来看以下的代码:
int a, b = 0;
short c = 10;
//c是short型变量。可是在此处会先将c转成int型变量。然后再和b进行加操作
a = b + c;
对类而言,也有这样的隐式类型转换,比方图27所看到的的代码:
图27 隐式类类型转换演示样例
图27中測试代码里。编译器进行了隐式类型转换,即先用常量2构造出一个暂时的TypeCastObj对象,然后再拷贝构造为obj2对象。注意。支持这样的隐式类型转换的类的构造函数须要满足一个条件:
假设构造函数有多个參数,则不能隐式转换。
注意:
TypeCastObj obj3(3) ;//这样的调用是直接初始化,不是隐式类型转换
假设程序猿不希望发生这样的隐式类型转换该怎么办?仅仅须要在类声明中构造函数前加入explicit关键词就可以,比方:
explicit TypeCastObj(intx) :mX(x){
cout<<"in ordinay constructor"<<endl;
}
struct是C语言中的古老成员了,在C中它叫结构体。只是到了C++世界。struct不再是C语言中结构体了。它升级成了class。即C++中的struct就是一种class,它拥有类的全部特征。只是,struct和普通class也有一点差别。那就是struct的成员(包括函数和变量)默认都是public的訪问权限。
对Java程序猿而言。操作符重载是一个陌生的话题。由于Java语言并不支持它[⑥]。
相反,C++则灵活非常多,它支持非常多操作符的重载。为什么两种语言会有如此大相径庭的做法呢?关于这个问题。前文也曾从面向对象和面向数据类型的角度探讨过:
上述“从面向对象的角度和从数据类型的角度看待是否应该支持操作符重载”的观点仅仅是笔者的一些看法。至于两种语言的设计者为何做出这样的选择,想必其背后都有充足的理由。
言归正传。先来看看C++中哪些操作符支持重载,哪些不支持重载。答案例如以下:
/* 此处内容为笔者加入的解释 */
能够被重载的操作符:
+ - * / % ^
&/*取地址操作符*/ | ~ ! , /*逗号运算符*/ =/*赋值运算符*/
< > < = >= ++ --
<</*输出操作符*/ >>/*输入操作符*/ == != && ||
+= -= /= %= ^= &=
|= *= <<= >>= []/*下标运算符*/ ()/*函数调用运算符*/
->/*类成员訪问运算符,pointer->member */
->*/*也是类成员訪问运算符,可是方法为pointer->*pointer-to-member */
/*以下是内存创建和释放运算符。
当中new[]和delete[]用于数组的内存创建和释放*/
new new[] delete delete[]
不能被重载的操作符:
::(作用域运算符) ?
:(条件运算符)
. /*类成员訪问运算符。object.member */
.* /*类成员訪问运算符,object.*pointer-to-member */
除了上面列出的操作符外,C++还能够重载类型转换操作符,比方:
class Obj{//Obj类声明
...
operator bool();//重载bool类型转换操作符。注意,没有返回值的类型
bool mRealValue;
}
Obj::operator bool(){ //bool类型转换操作符函数的实现,没有返回值的类型
return mRealValue;
}
Obj obj;
bool value = (bool)obj;//将obj转换成bool型变量
C++操作符重载机制非常灵活。绝大部分运算符都支持重载。这是好事,但同一时候也会因灵活过度造成理解和使用上的困难。
提示:
实际工作中仅仅有小部分操作符会被重载。关于C++中全部操作符的知识和演示样例,请读者參考http://en.cppreference.com/w/cpp/language/operators。
接着来看C++中操作符重载的实现方式。
操作符重载说白了就是将操作符当成函数来对待。当运行某个操作符运算时。相应的操作符函数被调用。和普通函数比起来,操作符相应的函数名由“operator 操作符的符号”来标示。
既然是函数。那么就有类的成员函数和非类的成员函数之分,C++中:
本节先来看一个能够採用两种方式来重载的加操作符的演示样例,如图28所看到的:
图28 Obj对+号的重载演示样例
图28中,Obj类定义了两个+号重载函数,分别实现一个Obj类型的变量和另外一个Obj类型变量或一个int型变量相加的操作。同一时候,我们还定义了一个针对Obj类型和布尔类型的+号重载函数。
+号重载为类成员函数或非类成员函数均可,程序猿应该依据实际需求来决定採用哪种重载方式。以下是一段測试代码:
Obj obj1, obj2;
obj1 = obj1+obj2;//调用Obj类第一个operator+函数
int x = obj1+100;//调用Obj类第二个operator+函数
x = obj1.operator+(1000); //显示调用Obj类第二个operator+成员函数
int z = obj1+true;//调用非类的operator+函数
强调:
实际编程中,加操作符通常会重载为类的成员函数。而且,输入參数和返回值的类型最好都是相应的类类型。由于从“两个整型操作数相加的结果也是整型”到“两个Obj类型操作数相加的结果也是Obj类型”的推导是非常自然的。上述演示样例中。笔者有意展示了操作符重载的灵活性。故而重载了三个+操作符函数。
本章非常多演示样例代码都用到了C++的标准输出对象cout。和标准输出对象相相应的是标准输入对象cin和标准错误输出对象cerr。当中。cout和cerr的类型是ostream,而cin的类型是istream。ostream和istream都是类名。它们和Java中的OutputStream和InputStream有些相似。
cout和cin怎样使用呢?来看以下的代码:
using std::cout;//cout,endl,cin都位于std命名空间中。
endl代表换行符
using std::endl;
using std:cin;
int x = 0, y =1;//定义x和y两个整型变量
cout <<”x = ” << x <<” y = ” << y << endl;
/*
上面这行代码表示:
1 将“x = ”字符串写到cout中
2 整型变量x的值写到cout中
3 “ y = ”字符串写到cout中
4 整型变量y的值写到cout中
5 写入换行符。终于,标准输出设备(通常是屏幕)中将显示:
x = 0 y = 1
*/
上面语句看起来比較奇妙,<<操作符竟然能够连起来用。这是怎么做到的呢?来看图29:
图29 等价转换
如图29可知。仅仅要做到operator <<函数的返回值就是第一个输入參数本身,我们就能够进行代码“浓缩”。那么,operator<<函数该怎么定义呢?非常easy:
ostream&operator<<(ostream& os,某种数据类型 參数名){
....//输出内容
return os;//第一个输入參数又作为返回值返回了
}
istream&operator>>(istream& is, 某种数据类型 參数名){
....//输入内容
return is;
}
通过上述函数定义。"cout<<....<<..."和"cin>>...>>.."这样的代码得以成功实现。
C++的>>和<<操作符已经实现了内置数据类型和某些类类型(比方STL标准类库中的某些类)的输出和输入。
假设想实现用户自己定义类的输入和输出则必须重载这两个操作符。
来看一个样例,如图30所看到的:
图30 <<和>>操作符重载演示样例
通过图30的重载。我们能够通过标准输入输出来操作Obj类型的对象了。
比較:
<<输出操作符重载有点相似于我们在Java中为某个类重载toString函数。
toString的目的是将类实例的内容转换成字符串以方便打印或者别的用途。
->和*操作符重载一般用于所谓的智能指针类,它们必须实现为类的成员函数。
在介绍相关演示样例代码前。笔者要特别说明一点:这两个操作符假设操作的是指针类型的对象,则并非重载,比方以下的代码:
//假设Object类重载了->和*操作符
Object *pObject =new Object();//new一个Object对象
//以下的->操作符并非重载。
由于pObject是指针类型,所以->仅仅是依照标准语义訪问它的成员
pObject->getSomethingPublic();
//同理,pObject是指针类型。故*pObject就是对该地址的解引用,不会调用重载的*操作符函数
(*pObject).getSomethingPublic();
依照上述代码所说,对于指针类型的对象而言,->和*并不能被重载。那这两个操作符的重载有什么作用?来看演示样例代码。如图31所看到的:
图31 ->和*操作符重载演示样例
图31中,笔者实现了一个用于保护某个new出来的Obj对象的SmartPointerOfObj类,通过重载SmartPointerOfObj的->和*操作符,我们就好像直接在操作指针型变量一样。在重载的->和*函数中,程序猿能够做一些检查和管理,以确保mpObj指向正确的地址。目的是避免操作无效内存。
这就是一个非常easy的智能指针类的实现。
提示:
STL标准库也提供了智能指针类。
ART中大量使用了它们。
本章兴许将介绍STL中的智能指针类。
使用智能指针还有一个优点。由于智能指针对象往往不须要用new来创建。所以智能指针对象本身的内存管理是比較简单的。不须要考虑delete它的问题。另外,智能指针的目标是更智能得管理它所保护的对象。借助它,C++也能做到一定程度的自己主动内存回收管理了。
比方图31中測试代码的spObj对象,它不是new出来的,所以当函数返回时它自己主动会被析构。而当它析构的时候,new出来的Obj对象又将被delete。所以这两个对象(new出来的Obj对象和在栈上创建的spObj对象)所占领的资源都能够完美回收。
new和delete操作符的重载与其它操作符的重载略有不同。寻常我们所说的new和delete实际上是指new表达式(expression)以及delete表达式,比方:
Object* pObject =new Object; //new表达式,对于数组而言就是new Object[n];
deletepObject;//delete表达式,对于数组而言就是delete[] pObject
上面这两行代码各自是new表达式和delete表达式。这两个表达式是不能自己定义的,可是:
2 new表达式运行过程中将首先调用operator new函数。而C++同意程序猿自己定义operatornew函数。
2 delete表达式运行过程的最后将调用operator delete函数,而程序猿也能够自己定义operatordelete函数。
所以。所谓new和delete的重载实际上是指operator new和operator delete函数的重载。
以下我们来看一下operator new和operator delete函数怎样重载。
提示:
为行文方便。下文所指的new操作符就是指operator new函数。delete操作符就是指operator delete函数。
我们先来看new操作符的语法。如图32所看到的:
图32 new的语法
new操作符一共同拥有12种形式。使用方法相当灵活。当中:
有些new函数会抛异常,只是笔者接触的程序中都没有使用过C++中的异常,所以本书不拟讨论它们。
请读者务必注意。假设我们在类中重载了随意一种new操作符。那么系统的new操作符函数将被隐藏。隐藏的含义是指编译器假设在类X中找不到匹配的new函数时,它也不会去搜索系统定义的匹配的new函数。这将导致编译错误。
注意:何谓“隐藏”?
http://en.cppreference.com/w/cpp/memory/new/operator_new提到了仅仅要类重载随意一个new函数。都将导致系统定义的new函数全部被隐藏。关于“隐藏”的含义,经过笔者測试。应该是指编译器假设在类中没有搜索到合适的new函数后。将不会主动去搜索系统定义的new函数,如此将导致编译错误。
假设不想使用类重载的new操作符的话。则必须通过::new的方式来强制使用全局new操作符。当中,::是作用域操作符,作用域能够是类(比方Obj::)、命名空间(比方stl::),或者全局(::前不带名称)。
综上所述,new操作符重载非常灵活。也非常easy出错。所以建议程序猿尽量不要重载全局的new操作符。而是尽可能重载特定类的new操作符(图32中的(9)到(12))。
接着来看delete操作符的语法,如图33所看到的:
图33 delete操作符的语法
delete使用方法比new还要复杂。
此处须要特别说明的是:
上面的描写叙述不太直观。我们通过一个样例进一步来解释它。如图34所看到的:
图34 delete操作符的使用方法演示样例
图34中:
这两个操作符函数最后一个參数都是bool型。
图34中还特别指出代码中不能直接使用delete p1这样的表达式,这会导致编译错误。提示没有匹配的delete函数,这是由于:
提示:
关于全局delete函数被隐藏的问题。读者最好还是动手一试。
如今我们来看new和delete操作符重载的一个简单演示样例。
如图35所看到的:
强调:
考虑到new和delete的高度灵活性以及和它们和内存分配释放紧密相关的重要性,程序猿最好仅仅针对特定类进行new和delete操作符的重载。
图35 new/delete操作符重载的演示样例
图35中。笔者为Obj重载了两个new操作符和两个delete操作符:
讨论:重载new和delete操作符的优点
通过重载new和delete操作符,我们有机会在对象创建和释放的时候做一些内存管理的工作。
比方,每次new一个Obj对象。我们递增new被调用的次数。delete的时候再递减。当程序退出时。我们检查该次数是否归0。
假设不为0,则表示有Obj对象没有被delete,这非常可能就是内存泄露的潜在原因。
我们用new表达式创建一个对象的时候,系统将在堆上分配一块内存。然后这个对象在这块内存上被构造。由于这块内存分配在堆上,程序猿一般无法指定其地址。
这一点和Java中的new相似。但有时候我们希望在指定内存上创建对象,能够做到吗?对于C++这样的灵活度非常高的语言而言,这个小小要求自然能够轻松满足。仅仅要使用特殊的new就可以:
2 void* operator new(size_t count, void* ptr):它是placement new中的一种。此函数第二个參数是一个代表内存地址的指针。该函数的默认实现就是直接将ptr作为返回的内存地址。也就是将传入的内存地址作为new的结果返回给调用者。
使用这样的方式的new操作符时。由于返回的内存地址就是传进来的ptr,这就达到了在指定内存上构造对象的功能。立即来看一个演示样例。如图36所看到的:
图36 new/delete演示样例
图36展示了placement new的使用方法,即在指定内存中构造对象。这个指定内存是在栈上创建的。另外,对于这样的方式创建的对象,假设要delete的话必需小心,由于系统提供的delete函数将回收内存。
在本例中。对象是构造在栈上的,其占领的内存随testPlacementNew函数返回后就自己主动回收了,所以图35中没有使用delete。只是请读者务必注意,这样的情况下内存不须要主动回收。可是对象是须要析构的。
显然,这样的仅仅有new没有delete的使用方法和寻经常使使用方法不太匹配。有点别扭。
怎样改进呢?方法非常easy,我们仅仅要按例如以下方式重载delete操作符。就能够在图35的实例中使用delete了:
//Class Obj重载delete操作符
void operator delete(void* obj){
cout<<"delete--"<<endl;
//return ::operator delete(obj);屏蔽内存释放,由于本例中内存在栈上分配的
}//读者能够自行改动測试案例以加深对new和delete的体会。
假设Obj类按如上方式重载了delete函数。我们在图36的代码中就能够“delete pObj1”了。
探讨:重载new和delete的优点
普通情况下,我们重载new和delete的目的是将内存创建和对象构造分隔开来。这样有什么优点呢?比方我们能够先创建一个大的内存,然后通过重载new函数将对象构造在这块内存中。当程序退出后,我们仅仅要释放这个大内存就可以。
另外,由于内存创建和释放与对象构造和析构分离了开来,对象构造完之后切记要析构,delete表达式仅仅是帮助我们调用了对象的析构函数。假设像本例那样根本不调用delete的话,就须要程序猿主动析构对象。
ART中,有些基础性的类重载了new和delete操作符,它们的实例就是用相似方式来创建的。以后我们会见到它们。
最后,new和delete是C++中比較复杂的一个知识点。
关于这一块的内容,笔者认为參考文献里列的几本书都没有说太清楚和全面。请意犹未尽的读者阅读例如以下两个链接的内容:
http://en.cppreference.com/w/cpp/memory/new/operator_new
http://en.cppreference.com/w/cpp/memory/new/operator_delete
函数调用运算符使得对象能像函数一样被调用,什么意思呢?我们知道C++和Java一样,函数调用的写法是“函数名(參数)”。假设我们把函数名换成某个类的对象,即“对象(參数)”。就达到了对象像函数一样被调用的目的。
这个过程得以顺利实施的原因是C++支持函数调用运算符的重载,函数调用运算符就是“()”。
来看一个样例,如图37所看到的:
图37 operator ()重载演示样例
图37展示了operator ()重载的演示样例:
2 此操作符的重载比較简单,就和定义函数一样能够依据须要定义參数和返回值。
2 函数调用操作符重载后。Obj类的实例对象就能够像函数一样被调用了。我们一般将这样的能像函数一样被调用的对象叫做函数对象。图37也提到。普通函数是没有状态的。可是函数对象却不一样。函数对象首先是对象,然后才是能够像函数一样被调用。
而对象是有所谓的“状态”的,比方图中的obj和obj1,两个对象的mX取值不同。这将导致外界传入一样的參数却得到不同的调用结果。
模板是C++语言中比較高级的一个话题。
羞愧得讲,笔者使用C++、Java这么些年。极少自己定义模板,最多就是在使用容器类的时候会接触它们。由于日常工作中用得非常少,所以对它的认识并不深刻。
这一次由于ART代码中大量使用了模板,所以笔者也算是被逼上梁山。从头到尾细致研究了C++中的模板。
介绍模板详细知识之前,笔者先分享几点关于模板的非常重要的学习心得:
面向对象最重要的一个特点就是抽象,即将公共的属性、公共的行为抽象到基类中去。
这样的抽象非常好理解,现实生活中也无处不在。反观模板,它事实上也是一种抽象,仅仅只是这样的抽象的关注点不在属性,不在行为,而在于数据类型。
比方。有一个返回两个操作数相加之和的函数,它即能够处理int型操作数,也能够处理long型操作数。
那么。从数据类型的角度进行抽象的话,我们能够用一个代表通用数据类型的T做为该函数的參数类型。该函数内部仅仅对T类型的变量进行相加。至于T详细是什么,此时不用考虑。
而使用这个函数的时候。当传入int型变量时,T就变成int。
当传入long型变量时,T就变成long。
所以,模板的重点在于将它所操作的数据的类型抽象出来。
模板实例化是编译器发现使用者用详细的数据类型来使用模板时,它就会将模板里的通用数据类型替换成详细的数据类型,从而生成实际的函数或类。
比方前面提到的两个操作数相加的模板函数。当传入int型变量时,模板会实例化出一个參数为int型的函数,当传入long型变量时,模板又会实例化出一个參数为long型的函数。
当然,假设没有地方用详细数据类型来使用这个模板。则编译器不会生成不论什么函数。注意,模板的实例化是由编译器来做的。但触发实例化的原因是由于使用者用详细数据类型来使用了某个模板。
简而言之。对于模板而言,程序猿须要重点关注两个事情,一个是对数据类型进行抽象,还有一个是利用详细数据类型来绑定某个模板以将事实上例化。
好了,让我们正式进入模板的世界,故事先从简单的函数模板開始。
提示:
模板编程是C++中非常难的部分,參考文献[4]用了六章来介绍与之相关的知识点。
无论怎样,模板的核心依旧是笔者前面提到的两点,一个是数据类型抽象,一个是实例化。
先来看函数模板的定义方法,如图38所看到的:
图38 函数模板的定义
图38所看到的为两个函数模板的定义,当中有几点须要读者注意:
模板參数列表不能为空。模板參数和函数參数有些相似,能够定义默认值。
比方图中add123最后一个模板參数T3。其默认值是long。
提示:
图38中的函数模板定义中,template能够和其后的代码位于同一行。比方:
template<typename T> T add(const T&a1,const T& a2);
建议开发人员将其分成两行,由于这样的代码阅读起来会更easy一些。
以下继续讨论template和模板參数:
首先。能够定义随意多个模板參数。模板參数也能够像函数參数那样有默认值。
其次,函数的參数都有数据类型。相似,模板參数(如上面的T)也有类型之分:
2 代表数据类型的模板參数:用typename关键词标示,表示该參数代表数据类型,实例化时应传入详细的数据类型。比方typename T是一个代表数据类型的模板參数,实例化的时候必须用数据类型来替代T(或者说。T的取值为数据类型。比方int,long之类的)。另外。typename关键词也能够用class关键词替代。所以"template<class T>"和"template<typenameT>"等价。建议读者尽量使用typename作为关键词。
2 非数据类型參数:非数据类型的參数支持整型、指针(包括函数指针)、引用。可是这些參数的值必须在实例化期间(也就是编译期)就能确定。
关于非类型參数,此处先展示一个简单的演示样例,兴许介绍类模板时会碰到详细使用方法。
//以下这段代码中。T是代表数据类型的模板參数,N是整型。compare则是函数指针
//它们都是模板參数。
template<typename T,int N,bool (*compare)(constT & a1,const T &a2)>
void comparetest(const T& a1,const T& a2){
cout<<"N="<<N<<endl;
compare(a1,a2);//调用传入的compare函数
}
图39所看到的为图38所定义的两个函数模板的实例化演示样例:
图39 函数模板的实例化
图39所看到的为add和add123这两个函数模板的实例化示意。结合前文反复强调的内容。函数模板的实例化就是当程序用详细数据类型来使用函数模板时,编译器将生成详细的函数:
这是编译器依据函数实參自己主动推导出来的。叫模板实參推导。
推导过程有一些规则,属于比較高级的话题。笔者不拟讨论。只是,不论推导规则有多复杂,其目的就是为了确定模板參数的详细取值情况。这一点请读者牢记。
比方②中所看到的的三个函数。
编译器将生成三个不同的add123函数。
假设add123函数模板中没有为T3设置默认类型的话。编译将出错。
上文介绍了函数模板的实例化,实例化就是指编译器进行类型推导,然后得到详细的函数。
实例化得到的这些函数除了数据类型不一样之外,函数内部的功能是全然一样的。有没有可能为某些特定的数据类型提供不一样的函数功能?
显然,C++是支持这样的做法的,这也被称为模板的特例化(英文简称specialization)。特例化就是当函数模板不太适合某些特定数据类型时。我们单独为它指定一套代码实现。
读者可能会认为非常奇怪,为什么会有这样的需求?以图38中的add123为例,假设程序猿传入的參数类型是指针的话,显然我们不能直接使用add123原函数模板的内容(那样就变成了两个指针值的相加),而应该单独实现一个针对指针类型的函数实现。要达到这个目的就须要用到特例化了。来看详细的做法。如图40所看到的:
图40 特例化演示样例
类模板的规则比函数模板要复杂,我们来看一个样例,如图41所看到的:
图41 类模板演示样例
图41中定义一个类模板。其语法格式和函数模板类型,classkeyword前须要由template<模板參数>来修饰。另外,类模板中能够包括普通的成员函数,也能够有成员模板。
这导致类模板的复杂度(包括程序猿阅读代码的难度)大大添加。
注意:
普通类也能包括成员模板,这和函数模板相似,此处不拟详述。
接着来看类模板的特例化,它分为全特化和偏特化两种情况,如图42所看到的:
图42 类模板的全特化和偏特化
图42展示了类模板的全特化和偏特化,当中:
偏特化也叫部分特例化(partial specialization)。
但笔者认为“部分特例化”有些言不尽意。由于偏特化不仅仅包括“为部分模板參数指定详细类型”这一种情况,它还能够为模板參数指定某些特殊类型,比方:
template<typename T> class Test{}//定义类模板Test。包括一个模板參数
//偏特化Test类模板。模板參数类型变成了T*。这就是偏特化的第二种表现形式
template<typename T> class<T*> Test{}
类模板的使用如图43所看到的:
图43 类模板使用演示样例
图43展示了类模式的使用演示样例。
当中,值得关注的是C++11中程序猿可通过using关键词定义类模板的别名。
而且,使用类模板别名的时候能够指定一个或多个模板參数。
最后,类模板的成员函数也能够在类外(即源文件)中定义,只是这会导致代码有些难阅读,图44展示了怎样在类外定义accessObj和compare函数:
图44 在源文件里定义类模板中的成员函数
图44中:
最后,关于类模板还有非常多知识。比方友元、继承等在类模板中的使用。本书对于这些内容就不拟一一道来,读者以后可在碰到它们的时候再去了解。
C++11引入了lambda表达式(lambda expression)。这比Java直到Java 8才正式在规范层面推出lambda表达式要早三年左右。
lambda表达式和还有一个耳熟能详的概念closure(闭包)密切相关,而closure最早被提出来的目的也是为了解决数学中的lambda演算(λ calculus)问题[⑧]。
从严格语义上来说,closure和lambda表达式并不全然同样,只是一般我们能够认为二者描写叙述得是同一个东西。
提示:closure和lambda的差别
关于二者的差别,读者可參考Effective C++作者Scott Meyers的一篇博文,地址例如以下:
http://scottmeyers.blogspot.com/2013/05/lambdas-vs-closures.html
我们在“函数调用运算符重载”一节中曾介绍过函数对象,函数对象是那些重载了函数调用操作符的类的实例。和普通函数比起来:
通过上面的描写叙述,我们知道函数对象的两个特点,一个是能够保存状态,另外一个是能够运行。
只是,和函数一样,程序猿要使用函数对象的话,首先要定义相应的类。然后才干创建该类的实例并使用它们。
如今我们来思考这样一个问题,可不能够不定义类,而是直接创建某种东西,然后能够运行它们?
以上问题的讨论就引出了C++中的lambda表达式,规范中没有明白说明lambda表达式是什么,但实际上它就是匿名函数对象。以下的代码展示了创建一个lambda表达式的语法结构:
auto f = [ 捕获列表,英文叫capture list ] ( 函数參数 ) ->返回值类型 { 函数体 }
当中:
尾置形式的函数返回声明即是把原来位于函数參数左側的返回值类型放到函数參数的右側。比方,"int func(int a){...} "的尾置声明形式为"autofunc(int a ) -> int {...}"。
当中,auto是关键词。用在此处表明该函数将採用尾置形式的函数返回声明。
以下我们通过样例进一步来认识lambda表达式,来看图45:
图45 lambda表达式演示样例(1)
图45 lambda表达式演示样例(2)
图45展示了lambda表达式的使用方法:
假设它通过引用方式捕获了一个函数内部的局部变量时,这个变量在跳出函数范围后将变得毫无意义,而且其占领的内存都可能不复存在了。
图45所看到的样例的捕获列表显示指定了要捕获的变量。假设变量比較多的话,要一个一个写上变量名会变得非常麻烦,所以lambda表达式还有更简单的方法来捕获全部变量,例如以下所看到的:
此处仅关注捕获列表中的内容
[=,&变量a,&变量b] = 号表示按值的方式捕获该lambda创建时所能看到的全部变量。
假设有些变量须要通过引用方式来捕获的话就把它们单独列出来(变量前带上&符号)
[&,变量a,变量b] &号表示按引用方式捕获该lambda创建时所能看到的全部变量。假设有些变量须要通过按值方式来捕获的话就把它们单独列出来(变量前不用带上=号)
STL是StandardTemplate Library的缩写。英文原意是标准模板库。由于STL把自己的类和函数等都定义在一个名为std(std即standard之意)的命名空间里。所以一般也称其为标准库。标准库的重要意义在于它提供了一套代码实现非常高效。内容涵盖很多基础功能的类和函数。比方字符串类,容器类,输入输出类,多线程并发类,经常使用算法函数等。尽管和Java比起来,C++标准库涵盖的功能并不算多,可是使用方法却非常灵活。学习起来有一定难度。
熟练掌握和使用C++标准库是一个合格C++程序猿的重要标志。
对于标准库。笔者感觉是越了解其内部的实现机制越能帮助程序猿更好得使用它。
所以,參考文献[2]差点儿是C++程序猿入门后的必读书了。
STL的内容非常多,本节仅从API使用的角度来介绍当中一些经常使用的类和函数。包括:
STL string类和Java String类非常像。只是,STL的string类事实上仅仅是模板类basic_string的一个实例化产物。STL为该模板类一共定义了四种实例化类。如图46所看到的:
图46 string的家族
图46中:
string类的完整API可參考http://www.cplusplus.com/reference/string/string/?
其使用和Java String有些相似,所以上手难度并不大。图47中的代码展示了string类的使用:
图47 string类的使用
好在Java中也有容器类,所以C++的容器类不会让大家感到陌生,表1对照了两种语言中常见的容器类。
表1 容器类对照
容器类型 |
STL类名 |
Java类(仅用于參考) |
说明 |
动态数组 |
vector |
ArrayList |
动态大小的数组,随机訪问速度快 |
链表 |
list |
LinkedList |
一般实现为双向链表 |
集合 |
set,multiset |
SortedSet |
有序集合。一般用红黑树来实现。set中没有值同样的多个元素。而multiset同意存储值同样的多个元素 |
映射表 |
map、multimap |
SortedMap |
按Key排序。一般用红黑树来实现。 map中不同意有Key同样的多个元素。而multimap同意存储Key同样的多个元素 |
哈希表 |
unordered_map |
HashedMap |
映射表中的一种,对Key不排序 |
本节主要介绍表1中vector、map这两种容器类的使用方法以及Allocator的知识。关于list、set和unordered_map的详细使用方法。读者可阅读參考文献[2]。
提示:
list、set和unordered_map的在线API查询链接:
list的API:http://en.cppreference.com/w/cpp/container/list
set的API:http://en.cppreference.com/w/cpp/container/set
unordered_map的API:http://en.cppreference.com/w/cpp/container/unordered_map
vector是模板类,使用它之前须要包括<vector>头文件。图48展示了vector的一些常见使用方法:
图48 vector使用方法演示样例
图48中有三个知识点须要读者注意:
C++中没有通用的Iterator类(Java有Iterator接口类),而是须要通过容器类::Iterator的方式定义该容器类相应的迭代器变量。迭代器用于訪问容器的元素。其作用和Java中的迭代器相似。
auto关键词的出现使得程序猿不用再写冗长的类型名了。一切交由编译器来完毕。
关于vector的知识我们就介绍到此。
注意:
再次提醒读者,STL容器类的学习绝非知道几个API就能够的,其内部有相当多的知识点须要注意才干真正用好它们。强烈建议有进一步学习欲望的读者研读參考文献[2]。
map也叫关联数组。图49展示了map类的情况:
图49 map类
图49中:
pair定义在头文件<utility中>。pair也是模板类。有两个模板參数T1和T2。
讨论:Compare和Allocator
map类的声明中,Compare和Allocator尽管都是模板參数。但非常明显不能随便给它们设置数据类型,比方Compare和Allocator都取int类型能够吗?当然不行。实际上,Compare应该被设置成这样一种类型,这个类型的变量是一个函数对象,该对象被运行时将比較两个Key的大小。map为Compare设置的默认类型为std::less<Key>。
less将按以小到大顺序对Key进行排序。
除了std::less外,还有std::greater,std::less_equal,std::greater_equal等。
同理,Allocator模板參数也不能随便设置成一种类型。后文将继续介绍Allocator。
图50展示了map类的使用方法:
图50 map的使用方法展示
图50定义了一个key和value类型都是string的map对象。有两种方法为map加入元素:
map默认的Compare模板參数是std::less。它将按从小到大对key进行排序,怎样为map指定其它的比較方式呢?来看图51:
图51 map的使用方法之Compare
图51展示了map中和Compare模板參数有关的使用方法。当中:
Java程序猿在使用容器类的时候从来不会考虑容器内的元素的内存分配问题。由于Java中,全部元素(除int等基本类型外)都是new出来的,容器内部无非是保存一个相似指针这样的变量。这个变量指向了真实的元素位置。
这个问题在C++中的容器类就没有这么简单了。
比方,我们在栈上构造一个string对象,然后把它加到一个vector中去。
vector内部是保存这个string变量的地址。还是在内部构造一个新的存储区域。然后将string对象的内容保存起来呢?显然。我们应该选择在内部构造一个区域,这个区域存储string对象的内容。
STL全部容器类的模板參数中都有一个Allocator(译为分配器)。它的作用包括分配内存、构造相应的对象,析构对象以及释放内存。STL为容器类提供了一个默认的类,即std::allocator。
其使用方法如图52所看到的:
图52 allocator的使用方法
图52展示了allocator模板类的使用方法,我们能够为容器类指定自己的分配器,它仅仅要定义图52中的allocate、construct、destory和deallocate函数就可以。当然,自己定义的分配器要设计好怎样处理内存分配、释放等问题也是一件非常考验程序猿功力的事情。
提示:
ART中也定义了相似的分配器,以后我们会碰到它们。
STL还为C++程序猿提供了诸如搜索、排序、拷贝、最大值、最小值等算法操作函数以及一些诸如less、great这样的函数对象。本节先介绍算法操作函数,然后介绍STL中的函数对象。
STL中要使用算法相关的API的话须要包括头文件<algorithm>。假设要使用一些专门的数值处理函数的话则需额外包括<numeric>头文件。參考文献[2]在第11章中对STL算法函数进行了细致的分类。只是本节不打算从这个角度、大而全得介绍它们,而是将ART中经常使用的算法函数挑选出来介绍,如表2所看到的。
表2 ART源代码中经常使用的算法函数
函数名 |
作用 |
fill fill_n |
fill:为容器中指定范围的元素赋值 fill_n:为容器内指定的n个元素赋值 |
min/max |
返回容器某范围内的最小值或最大值 |
copy |
拷贝容器指定范围的元素到另外一个容器 |
accumulate |
定义于<numerics>,计算指定范围内元素之和 |
sort |
对容器类的元素进行排序 |
binary_search |
对已排序的容器进行二分查找 |
lexicographical_compare |
按字典序对两个容器内内指定范围的元素进行比較 |
equal |
推断两个容器是否同样(元素个数是否相等,元素内容是否同样) |
remove_if |
从容器中删除满足条件的元素 |
count |
统计容器类满足条件的元素的个数 |
replace |
替换容器类旧元素的值为指定的新元素 |
swap |
交换两个元素的内容 |
图53展示了表2中一些函数的使用方法:
图53 fill、copy和accumulate等算法函数演示样例
图53中包括一些知识点须要读者了解:
这样的方式将算法和容器进行了最大程度的解耦,从此,算法无需关心容器,而是仅仅通过迭代器来获取、操作元素。
以copy为例。它将源容器指定范围元素复制到目标容器中去。只是,目标容器必须要保证有足够的空间能够容纳待拷贝的源元素。
比方图中aIntVector有6个元素。可是bIntVector仅仅有0个元素,aIntVector这6个元素能复制到bIntVector里吗?copy函数不能回答这个问题。仅仅能由程序猿来保证目标容器有足够的空间。这导致程序猿使用copy的时候就非常头疼了。为此,STL提供了一些辅助性的迭代器封装类。比方back_inserter函数将返回这样一种迭代器,它会往容器尾部加入元素以自己主动扩充容器的大小。如此,使用copy的时候我们就不用操心目标容器容量不够的问题了。
比方第二个accumulate函数的最后一个參数,我们为其指定了一个lambda表达式用于计算两个元素之和。
提示:
STL的迭代器也是非常重要的知识点。由于本书不拟介绍它。请读者阅读相关參考文献。
接着来看图54。它继续展示了算法函数的使用方法:
图54 sort、binary_search等函数使用演示样例
图54中remove_if函数向读者生动展示了要了解STL细节的重要性:
可是这个元素会被remove到哪去?该元素所占的内存会不会被释放?STL中。remove_if函数仅仅是将符合remove条件的元素挪到容器的后面去。而将不符合条件的元素往前挪。所以,vector终于的元素布局为前面是无需移动的元素,后面是被remove的元素。可是请注意。vector的元素个数并不会发生改变。所以,remove_if将返回一个迭代器位置,这个迭代器的位置指向被移动的元素的起始位置。
即vector中真正有效的元素存储在begin()和newEnd之间,newEnd和end()之间是逻辑上被remove的元素。
是不是有种要抓狂的感觉?这个问题怎么破解呢?当使用者remove_if调用完毕后。务必要通过erase来移除容器中逻辑上不再须要的元素。代码例如以下:
//newEnd和end()之间是逻辑上被remove的元素。我们须要把它从容器里真正移除!
aIntVector.erase(newEnd,aIntVector.end());
最后,关于<algorithm>的全部内容请读者參考:
http://en.cppreference.com/w/cpp/header/algorithm
STL中要使用函数对象相关的API的话须要包括头文件<functional>,ART中经常使用的函数对象如表3所看到的。
表2 ART源代码中经常使用的算法函数
类或函数名 |
作用 |
bind |
对可调用对象进行參数绑定以得到一个新的可调用对象。详情见正文 |
function |
模板类。图51中介绍过。用于得到一个重载了函数调用对象的类 |
hash |
模板类。用于计算哈希值 |
plus/minus/multiplies |
模板类,用于计算两个变量的和,差和乘积 |
equal_to/greater/less |
模板类,用于比較两个数是否相等或大小 |
函数对象的使用相对照较简单,图55、图56给出了几个演示样例:
图55 bind函数使用演示样例
图55重点介绍了bind函数的使用方法。如图中所说。bind是一个非常奇特的函数,其主要作用就是对原可调用对象进行參数绑定从而得到一个新的可调用对象。bind的參数绑定规则须要了解。另外,占位符_X定义在std下的placeholders命名空间中,所以一般要用placeholders::_X来訪问占位符。
图56展示了有关函数对象的其它一些简单演示样例:
图56 函数对象的其它用例
图56展示了:
最后,关于<algorithm>的全部内容,请读者參考:
http://en.cppreference.com/w/cpp/header/functional
提示:
从容器类和算法以及函数对象来看。STL的全称标准模板库是非常名符事实上的,它充分利用了和发挥了模板的威力。
我们在本章1.3.3“->和*操作符重载”一节中曾介绍过智能指针类。C++11此次在STL中推出了两个比較经常使用的智能指针类:
该内存资源直到引用计数变成0时才会被释放。
被保护的内存资源仅仅能赋给一个unique_ptr对象。当unique_ptr对象销毁、重置时,该内存资源被释放。一个unique_ptr源对象赋值给一个unique_ptr目标对象时,内存资源的管理从源对象转移到目标对象。
shared_ptr和unqiue_ptr的思想事实上都非常easy。就是借助引用计数的概念来控制内存资源的生命周期。相比shared_ptr的共享式指针管理,unique_ptr的引用计数最多仅仅能为1罢了。
注意:环式引用问题
尽管有shared_ptr和unique_ptr,可是C++的智能指针依旧不能做到Java那样的内存自己主动回收。而且,shared_ptr的使用也必须非常小心。由于单纯的借助引用计数无法解决环式引用的问题。即A指向B,B指向A。可是没有别的其它对象指向A和B。这时。由于引用计数不为0,A和B都不能被释放。
以下分别来看shared_ptr和unique_ptr的使用方法。
图57为shared_ptr的使用方法演示样例,难度并不大:
图57 shared_ptr使用方法演示样例
图57中:
关于shared_ptr很多其它的信息,请參考:http://en.cppreference.com/w/cpp/memory/shared_ptr
ART中使用unique_ptr远比shared_ptr多,它的使用方法比shared_ptr更简单,如图58所看到的:
图58 unique_ptr使用方法演示样例
关于unique_ptr完整的API列表,请參考http://en.cppreference.com/w/cpp/memory/unique_ptr
本章对STL进行了一些非常粗浅的介绍。结合笔者个人的学习和使用经验,STL初看起来是比較easy学的。由于它很多其它关注的是怎样使用STL定义好的类或者函数。从“使用现成的API”这个角度来看。有Java经验的读者应该毫不陌生。由于Java平台从诞生之初就提供了大量的功能类,熟练的java程序猿使用它们时早已能做到信手拈来。同理。C++程序猿初学STL时。最開始仅仅要做到会查阅API文档,了解API的使用方法就可以。
可是,正如前面介绍copy、remove_if函数时提到的那样,STL的使用远比掌握API的使用方法要复杂得多。STL假设要真正学好、用好,了解其内部大概的实现是非常重要的。而且,这个重要性不仅停留在“能够写出更高效的代码”这个层面上,它更可能涉及到“避免程序出错。内存崩溃等各种莫名其妙的问题”上。这也是笔者反复强调要学习參考文献[2]的重要原因。另外,C++之父编写的參考文献[3]在第IV部分也对STL进行了大量深入的介绍,读者也能够细致阅读。
要研究STL的源代码吗?
对绝大部分开发人员而言,笔者认为研究STL的源代码必要性不大。
http://en.cppreference.com站点中会给出有些API的可能实现,读者查找API时最好还是了解下它们。
本节介绍ART代码中其它一些常见知识。
initializer_list和C++11中的一种名为“列表初始化”的技术有关。
什么是列表初始化呢?来看一段代码:
vector<int>intvec = {1,2,3,4,5};
vector<string>strvec{”one”,”two”,”three”};”
上面代码中,intvect和strvect的初值由两个花括号{}和里边的元素来指定。C++11中,花括号和当中的内容就构成一个列表对象。其类型是initializer_list。也属于STL标准库。
initializer_list是一个模板类,花括号里的元素的类型就是模板类型。而且。列表中的元素的数据类型必须同样。
另外。假设类创建的对象实例构造时想支持列表方式的话,须要单独定义一个构造函数。我们来看几段代码:
class Test{
public:
//①定义一个參数为initializer_list的构造函数
Test(initializer_list<int> a_list){
//②遍历initializer_list。它也是一种容器
for(auto item:a_list){
cout<<”item=”<<item<<endl;
} } }
Test a = {1,2,3,4};//仅仅有Test类定义了①,才干使用列表初始化构造对象
initializer_list<string> strlist ={”1”,”2”,”3”};
using ILIter =initializer_list<string>::iterator;
//③通过iterator遍历initializer_list
for(ILIter iter =strlist.begin();iter != strlist.end();++iter){
cout<<”item = ” << *iter<< endl;
}
enum应该是广大程序猿的老相识了,它是一个非常古老,使用广泛的关键词。只是。C++11中enum有了新的变化,我们通过两段代码来了解它:
//C++11之前的传统enum,C++11继续支持
enum Color{red,yellow,green};
//C++11之后,enum有一个新的形式:enum class或者enum struct
enum class ColorWithScope{red,yellow,green}
由上述代码可知,C++11为古老的enum加入了一种新的形式,叫enum class(或enum struct)。
enum class和Java中的enum相似,它是有作用域的,比方:
//对传统enum而言:
int a_red = red;//传统enum定义的color仅仅是把一组整型值放在一起罢了
//对enum class而言,必须按以下的方式定义和使用枚举变量。
//注意,green是属于ColorWithScope范围内的
ColorWithScopea_green = ColorWithScope::green;//::是作用域符号
//还能够定义另外一个NewColor。这里的green则是属于AnotherColorWithScope范围内
enum class AnotherColorWithScope{green,red,yellow};
//同样的做法对传统enum就不行,比方以下的enum定义将导致编译错误。
//由于green等已经在enum Color中定义过了
enum AnotherColor{green,red,yellow};
const一般翻译为常量。它和Java中的final含义一样,表示该变量定义后不能被改动。但C++11在const之外又提出了一个新的关键词constexpr。它是constexpression(常量表达式)的意思。constexpr有什么用呢?非常easy,就是定义一个常量。
读者一定会认为奇怪,const不就是用于定义常量的吗。为什么要再来一个constexpr呢?关于这个问题的答案。让我们通过样例来回答。
先看以下两行代码:
const int x = 0;//定义一个整型常量x,值为0
constexpr int y =1; //定义一个整型常量y,值为1
上面代码中,x和y都是整型常量,可是这样的常量的初值是由字面常量(0和1就是字面常量)直接指定的。
这样的情况下。const和constexpr没有什么差别(注意。const和constexpr的变量在指向指针或引用型变量时,二者还是有差别,此处不表)。
只是,对于以下一段代码,二者的差别立即显现了:
int expr(int x){//測试函数
if(x == 1) return 0;
if(x == 2) return 1;
return -1;
}
const int x = expr(9);
x = 8;//编译错误,不能对仅仅读变量进行改动
constexpr int y = expr(1);//编译错误,由于expr函数不是常量表达式
上面代码中:
非常显然。编译器推断expr不是常量表达式。由于它的返回值受输入參数的影响。所以上述y变量定义的那行代码将无法通过编译。
所以,constexpr关键词定义的变量一定是一个常量。
假设等号右边的表达式不是常量,那么编译器会报错。
提示:
常量表达式的推导工作是在编译期决定的。
assert,也叫断言。程序猿一般在代码中一些关键地方加上assert语句用以检查參数等信息是否满足一定的要求。
假设要求达不到。程序会输出一些警告语(或者直接异常退出)。总之,assert是一种程序运行时做检查的方法。
有没有一种方法能够让程序猿在代码的编译期也能做一些检查呢?为此。C++11推出了static_assert,它的语法例如以下:
static_assert (bool_constexpr , message )
当bool_constexpr返回为false的时候,编译器将报错。报错的内容就是message。
注意,这都是在编译期间做的检查。
读者可能会好奇,什么场合须要做编译期检查呢?举个最简单的样例。假设我们编写了一段代码,而且希望它仅仅能在32位的机器上才干编译。这时就能够利用static_assert了,方法例如以下:
static_assert(sizeof(void*) == 4,”can only be compiled in32bit machine”);
包括上述语句的源代码文件在64位机器上进行编译将出错。由于64位机器上指针的字节数是8,而不是4。
本章对C++语言(以C++11的名义)进行了浮光掠影般的介绍。
其内容不全面。细节不深入,描写叙述更谈不上精准。只是。本章的目的在于帮助Java程序猿、不熟悉C++11可是接触过C++98/03的程序猿对C++11有一个直观的认识和了解,这样我们将来分析ART代码时才不会认为陌生。对于那些有志于更进一步学习C++的读者们,以下列出的五本參考书则是不可缺少的。
作者是Stanley B.Lippman等人。译者为王刚,杨巨峰等。由电子工业出版社出版。假设对C++全然不熟悉,建议从这本书入门。
作者是Nicolai M.Josuttis。此书中文版译者是台湾著名的IT作家侯捷。C++标准库即是TL(Standard Template Library,标准模板库)。相比Java这样的语言。C++事实上也提供了诸如容器,字符串。多线程操作(C++11才正式提供)等这样的标准库。
作者是C++之父Bjarne Stroustrup,眼下仅仅有英文版。这本书写得非常细。由于是英文版,所以读起来也相对费事。另外。书里的演示样例代码有些小错误。
作者Anthony Williams。C++11标准库添加了对多线程编程的支持,假设打算用C++11标准库里的线程库,请读者务必阅读此书。这本书眼下仅仅有英文版。说实话,笔者看完这本书前5章后就不打算继续看下去了。由于C++11标准库对多线程操作进行了高度抽象的封装。这导致用户在使用它的时候还要额外去记住C++11引入的特性,非常麻烦。所以。我们在ART源代码中发现谷歌并未使用C++11多线程标准库。而是直接基于操作系统提供的多线程API进行了简单的,面向对象的类封装。
作者是Mical Wang和IBM XL编译器中国开发团队,机械工业出版社出版。
作者祁宇,机械工业出版社出版
[5],[6]这两本书都是由国人原创。语言和行文逻辑更符合国人习惯。相比前几本而言,这两本书主要集中在C++11的新特性和应用上。读者最好先有C++11基础再来看这两本书。
建议读者先阅读[5]。注意。[5]还贴心得指出每个C++11的新特性适用于那种类别的开发人员,比方全部人,部分人,类开发人员等。全部,读者应该依据自己的须要。选择学习相关的新特性,而不是尝试一股脑把全部东西都学会。
[①] C++98规范是于1998年落地的关于C++语言的第一个国际标准(ISO/IEC15882:1998)。而C++03则是于2003年定稿的第二个C++语言国际标准(ISO/IEC15882:2003)。由于C++03仅仅是在C++98上添加了一些内容(主要是新增了技术勘误表,Technical Corrigendum 1,简称TC1)。所以之后非常长一段时间内。人们把C++规范通称为C++98/03。
[②] 无独有偶,C++之父Bjarne Stroustrup也曾说过“C++11看起来像一门新的语言”[3]。
[③] 什么样的系统算业务系统呢?笔者也没有非常好的划分标准。只是以Android为例,LinuxKernel,视音频底层(Audio,Surface,编解码),OpenGLES等这些对性能要求非常高。和硬件平台相关的系统可能都不算是业务系统。
[④] 没有nullptr之前,系统或程序猿往往会定义一个NULL宏,比方#define NULL (0),只是这样的方式存在一些问题,所以C++11推出了nullptr关键词。
[⑤] 尽管代码中使用的是引用,但非常多编译器事实上是将引用变成了相应的指针操作。
[⑥] Java中,String对象是支持+操作的,这也许是Java中唯一的“操作符重载”的案例。
[⑦] 此处描写叙述并不全然准确。
对于STL标准库中某些类而言,<<和>>是能够实现为类的成员函数的。但对于其它类。则不能实现为类的成员函数。
[⑧] 关于closure的历史,请阅读https://en.wikipedia.org/wiki/Closure_(computer_programming)
[⑨] 编译器可能会将lambda表达式转换为一个重载了函数调用操作符的类。如此,变量f就是该类的实例,其数据类型随之确定。
标签:vector www div 定义函数 决定 virt binary 不同的 fonts
原文地址:https://www.cnblogs.com/fnlingnzb-learner/p/9499430.html