C++11学习

转自: 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++而言,开发确实方便太多。比方:

  • Java天生就是跨平台的。开发人员无需考虑操作系统,硬件平台的差异。

    而C++开发则高度依赖于操作系统以及硬件平台。比方Windows的C++程序到Linux平台上差点儿都无法直接使用。这当中的问题倒也不能全赖在C++语言本身上。

    仅仅是选择一门开发语言不仅仅是选择语言本身,其背后的生态系统(OS。硬件平台,公共类库,开发资源,文档等)随之也被选择。

  • 开发人员无需考虑内存管理。尽管Java也有内存泄露之说,但至少在开发过程中,开发人员不用斤斤计较于C++编程中必须要时刻考虑的“内存是否会泄露”,“对象被delete后是否会导致其它使用者操作无效内存地址”等问题。
  • 最后也是最重要的一点,Java有非常丰富的类库,诸如网络操作类,容器类,并发类。XML解析类等等等等。正是有了这些丰富的类库,才使得业务系统开发人员能聚焦在怎样利用这些现成的工具、类库来开发自己的业务系统,而不是从头到脚得反复制造车轮。

    比方,当年我在Windows搞一套C++封装的多线程工具类。之后移植到Linux上又得搞一套。而且还要花非常多精力维护它们。

个人感受:

我个人对C++是没有不论什么偏好的。

之所以用C++,非常大程度上是由于直接领导的选择。作为一个工作多年的老员工。在他印象里,那个年代的Java性能非常差。比不得C++的机灵和高效。另外,由于我们做得是高性能视音频数据网络传输(在局域网/广域网,几个GB的视音频文件相似FTP这样的上传下载),C++貌似是当时唯一能同一时候和“面向对象”。“性能不错”挂上钩的语言了。

在研究ART的时候,笔者发现其源代码是用一种和我曾经熟悉得C++差别非常大的C++语言编写得。这样的差别甚至一度让我感叹“不太认识C++语言了”。

后来,我才了解到这样的“全新的”C++就是C++11。当时我就在想,包括我自己在内,以及本书的读者们要不要学习它呢?思来覆去,我认为还是有这个必要:

  • 从Android 6.0源代码来看。native模块改用C++11来编写已成趋势。所以我们须要尽快了解C++11。为将来的学习和工作做准备。
  • 既然C++之父都说“C++11看起来像一门新的语言[6]”,那么我们全然能够把它当做一门新的语言来学习,而不用考虑是否有过C/C++基础的问题。这给了我们一个非常好的学习机会。

既然下定决心,那么就立即開始学习。正式介绍C++11前。笔者要特别强调以下几点注意事项:

  • 编程语言学习。以实用为主。

    所以本章所介绍的C++11内容,一切以看懂ART源代码为最高目标。

    源代码中没有涉及的C++11知识。本章尽量不予介绍。一些细枝末节,或者高深精尖的使用方法。笔者也不拟详述。假设读者想深入研究,最好还是阅读本章參考文献所列出的六本C++专著。

  • 学习是一个循序渐进的过程。对于刚開始学习的人而言,应首先以看懂C++11代码为主,然后才干尝试模仿着写,直到全然自己写。

    用C++敲代码,会碰到非常多所谓的“坑”。仅仅有亲历并吃过亏之后,才干深刻掌握这门语言。所以,假设读者想真正学好C++。那么一定要多写代码。不能停留在看懂代码的水平上。

注意:

最后。本章不是专门来讨论C++语法的,它更大的作用在于帮助读者更快得了解C++。

故笔者会尝试採用一些通俗的语言来介绍它。因此,本章在关于C++语法描写叙述的精准性上必定会有所不足。在此。笔者一方面请读者谅解,还有一方面请读者及时反馈所发现的问题。

以下。笔者将正式介绍C++11,本章拟解说例如以下内容:

  •  数据类型
  • C++源代码构成及编译
  •  Class
  • 操作符重载
  • 函数模板与类模板
  •  lambda表达式
  •  STL介绍
  • 其它一些经常使用知识点

1.1  数据类型

学习一门语言。首先从它定义的数据类型開始。

本节先介绍C++基本内置的数据类型。

1.1.1  基本内置数据类型介绍

图1所看到的为C++中的基本内置数据类型(注意,图中没有包括全部的内置数据类型):

图1  C++基本数据类型

图1展示了C++语言中几种经常使用的基本数据类型。有几点请读者注意:

  • 由于C++和硬件平台关联较大,规范没办法像Java那样严格规定每种数据类型所需的字节数,所以它仅仅定义了每种数据类型最少须要多少字节。比方,规范要求一个int型整数至少占领2个字节(只是,绝大部分情况下一个int整数将占领4个字节)。
  • C++定义了sizeof操作符。通过这个操作符能够得到每种数据类型(或某个变量)占领的字节个数。
  • 对于浮点数,规范仅仅要求最小的有效数字个数。对于单精度浮点数float而言。要求最少支持6个有效数字。

    对于双精度浮点数double类型而言。要求最少支持10个有效数字。

注意:

本章中,笔者可能会经常拿Java语言做对照。由于了解语言之间的差异更有助于高速掌握一门新的语言。

和Java不同的是,C++中的数据类型分无符号和有符号两种,比方:

图2  无符号数据类型定义

注意,无符号类型的关键词为unsigned

1.1.2  指针、引用和void类型

如今来看C++里另外三种经常使用的数据类型:指针、引用和void。如图3所看到的:

图3  指针、引用和void

由图3可知:

  • 指针类型的书写格式为T *。当中T为某种数据类型。
  • 引用类型的书写格式为T &,当中T为某种数据类型。
  • void代表空类型。也就是无类型。

    这样的类型仅仅能用于定义指针变量。比方void*。当我们确实不关注内存中存储的数据究竟是什么类型的话。就能够定义一个void*类型的指针来指向这块内存。

  • C++11開始。空指针由新keywordnullptr[④]表示,相似于Java中的null

以下我们着重介绍一下指针和引用。先来看指针:

1.  指针

关于指针。读者仅仅须要掌握三个基本知识点就能够了:

  • 指针的类型。
  • 指针的赋值。
  • 指针的解引用。
(1)  指针的类型

指针本质上代表了虚拟内存的地址。简单点说。指针就是内存地址。比方,在32位系统上。一个进程的虚拟地址空间为4G,虚拟内存地址从0x00xFFFFFFFF,这个段中的不论什么一个值都是内存地址。

一个程序运行时。其虚拟内存中会有什么呢?肯定有数据和代码。假设某个指针指向一块内存,该内存存储的是数据,C++中数据都得有数据类型。所以,指向这块内存的指针也应该有类型。

比方:

² int* p。变量p是一个指针,它指向的内存存储了一个(对于数组而言,就是一组)int型数据。

² short* p,变量p指向的内存存储了一个(或一组)short型数据。

假设指针相应的内存中存储的是代码的话,那么指向这块代码入口地址(代码往往是封装在函数里的,代码的入口就是函数的入口)的指针就叫函数指针。

函数指针的定义看起来有些古怪,如图4所看到的:

图4  函数指针定义演示样例

提示:

函数指针的定义语法看起来比較奇特。笔者也是实践了非常多次才了解它。

(2)  指针的赋值

定义指针变量后,下一个要考虑的问题就是给它赋什么值。

来看图5:

图5  指针变量的赋值

结合图5可知,指针变量的赋值有几种形式:

  • 直接将一个固定的值(比方0x123456)作为地址赋给指针变量。这样的做法非常危急。除非明白知道这块内存的作用以及所存储的内容,否则不能使用这样的方法。
  • 通过new操作符在堆上分配一块内存。该内存的地址存储在相应的指针变量中。
  • 通过取地址符&对获取某个变量或者函数的地址。

注意

函数指针变量的赋值也能够直接使用目标函数名。也可使用取地址符&。二者效果一致

(3)  指针的解引用

指针仅仅是代表内存的某个地址,怎样获取该地址相应内存中的内容呢?C++提供了解指针引用符号*来帮助大家。如图6所看到的:

图6  指针解引用

图6中:

  • 对于数据类型的指针,解引用意味着获取相应地址中内存的内容。
  • 对于函数指针,解引用意味着调用这个函数。  

讨论:

为什么C/C++中会有指针呢?由于C和C++语言作为系统编程(System Programming)语言,出于运行效率的考虑。它提供了指针这样的机制让程序猿能够直接操作内存。当然,这样的做法的利弊已经讨论了几十年,其主要坏处就在于大部分程序猿管不好内存。导致经常出现内存泄露,訪问异常内存地址等各种问题。

2.  引用

相比C。引用是C++特有的一个概念。我们来看图7,它展示了指针和引用的差别:

图7  引用的使用方法演示样例(1)

图7  引用的使用方法演示样例(2)

由图7可知:

  • 引用仅仅是变量的别名。由于是别名,所以C++要求在定义引用型变量时就必须将它和实际变量绑定。
  • 引用型变量绑定实际变量之后。这两个变量(原变量和它的引用变量)事实上就代表同一个东西了。图7中(1)以鲁迅为例,“鲁迅”和“周树人”都是同一个人。

C语言中没有引用。一样工作得非常好。那么C++引入引用的目的是什么呢[⑤]

  • 既然是别名,那么给原变量换一个更动听的名字可能是一个作用。
  • 比較图7中(2)的changeRefchangeNoRef可知。当函数的形參为引用时,函数内部对该形參的改动就是对实參的改动。
  • 再次强调。对于引用类型的形參而言,函数调用时,形參就变成了实參的别名。
  • 比較图7中(2)的changeRefchangePointers可知,指针型变量书写起来须要使用解地址引用*符号。不太方便。
  • 引用和原变量是一对一的强关系,而指针则能够随意赋值,甚至还能够通过类型转换变成别的类型的指针。在实际编码过程中,一对一的强关系能降低一些错误的发生。

和Java比較

和Java语言比起来,假设Java中函数的形參是基础类型(如int,long之类的)。则这个形參是传值的,与图7中的changeNoRef相似。假设这个函数的形參是类类型。则该形參相似于图7中的changeRef。在函数内部改动形參的数据,实參的数据相应会被改动。

1.1.3  字符和字符串

图8所看到的为字符和字符串的演示样例:

图8  字符和字符串演示样例

请读者注意图8中的Raw字符串定义的格式。它的标准格式为R"附加界定符(字符串)附加界定符"。附加界定符能够没有。

而笔者设置图8中的附加界定符为"**123"。

Raw字符串是C++11引入的,它是为了解决正則表達式里那些烦人的转义字符\而提供的解决方法。来看看C++之父给出的一个样例,有这样一个正則表達式('(?:[ˆ\\']|\\.)∗'|"(?:[ˆ\\"]|\\.)∗")|)

  • 在C++中。假设使用转义字符串来表达。则变成('(?:[ˆ\\\\']|\\\\.)∗'|\"(?:[ˆ\\\\\"]|\\\\.)∗\")|。

    使用转义字符后,整个字符串变得非常难看懂了。

  • ²假设使用Raw字符串。改成R"dfp(('(?

    :[ˆ\\']|\\.)∗'|"(?:[ˆ\\"]|\\.)∗")|)dfp"就可以。此处使用的界定字符为"dfp"。

非常显然。使用Raw字符串使得代码看起来更清爽,出错的可能性也降低非常多。

1.1.4  数组

直接来看关于数组的一个演示样例。如图9所看到的:

图9  数组演示样例

由图9可知:

  • 定义数组的语法格式为T name[数组大小]

    数组大小能够在编译时由初值列表的个数决定,也能够是一个常量。总之,这样的类型的数组。其数组大小必须在编译时决定。

  •  动态数组由new的方式在运行时创建。动态数组在定义的时候就能够通过{}来赋初值。

    程序中,代表动态数组的是一个相应类型的指针变量。

    所以,动态数组和指针变量有着天然的关系。

和Java比較

Java中,数组的定义方式是T[]name。笔者认为这样的书写方式比C++的书写方式要形象一些。

另外。Java中的数组都是动态数组。

了解完数据类型后,我们来看看C++中源代码构成及编译相关的知识。

1.2  C++源代码构成及编译

源代码构成是指怎样组织、管理和编译源代码文件。作为对照,我们先来看Java是怎么处理的:

  • Java中,代码仅仅能书写在以.java为后缀的源文件里。
  • Java中,每个Java源文件必须包括一个和文件同名的class。比方A.java必须定义公开的class A(或者是interface A)。
  • 绝大部分情况下,class A隶属于一个package。

    所以class A的全路径名为xx.yy.zz.A。当中,xx.yy.zz是包名。

  • 同一个package下的class B假设要使用class A的话,能够直接使用类A。假设class B位于别的package下的话。那么必须使用A的全路径名xx.yy.zz.A。当然,为了降低书写A所属包名的工作量。class B会通过import xx.yy.zz.A引入全路径名。然后,B也能直接使用类A了。

综其所述,源代码构成主要讨论两个问题:

  • 代码写在什么地方?Java中是放入.java为后缀的文件里。
  • 怎样解决不同源代码文件里的代码之间相互引用的问题?Java中,同package下。源文件A的代码能够直接使用源文件B的内容。

    不同package下,则必须通过全路径名訪问另外一个Package下的源文件A的内容(通过import能够降低书写包名的工作量)。

如今来看C++的做法:

  • 在C++中。承载代码的文件有头文件和源文件的差别。头文件的后缀名一般为.h

    也能够.hpp.hxx结尾。源文件以.cpp.cxx.cc结尾。

    仅仅要开发人员之间约定好,採用什么形式的后缀都能够。笔者个人喜欢使用.h.cpp做后缀名。而art源代码则以.h.cc为后缀名。

  • 一般而言,头文件里声明须要公开的变量,函数或者类。源文件则定义(或者说实现)这些变量,函数或者类。那些须要使用这些公开内容的代码能够通过#include方式将其包括进来。注意,由于C++中头文件和源文件都能够承载代码。所以头文件和源文件都能够使用#include指令。比方,源文件a.cpp能够#include"b.h",从而使用b.h里声明的函数。变量或者类。头文件c.h也能够#include "b.h"

以下我们分别通过头文件和源文件的几个演示样例来强化对它们的认识。

1.2.1  头文件演示样例

图10所看到的为一个非常easy头文件演示样例:

图10  Type.h演示样例

以下来分析图10中的Type.h:

  • 首先,C++中。头文件的写法有一定规则须要遵循。比方图10中的
  • #ifndef _TYPE_H_:ifndef是if not define之意。_TYPE_H_是宏的名称。
  • #define _TYPE_H_:表示定义一个名为_TYPE_H_的宏、
  • #endif:和前面的#ifndef相应。

这三个宏合起来的意思是,假设未定义_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函数:

  • C++中的命名空间和Java中的package相似。可是要求上要简单非常多。命名空间是一个范围(Scope),能够出如今随意头文件,源文件里。

    凡是放在某个命名空间里的函数,类,变量等就属于这个命名空间。

  • Type.h仅仅是声明(declare)了test函数,但没有这个函数的实现。声明仅是告诉编译器。我们有一个名叫test的函数。可是这个函数在什么地方呢?这时就须要有一个源文件来定义test函数。也就是实现test函数。

以下我们来看一个源文件演示样例:

1.2.2  源文件演示样例

源文件演示样例一如图11所看到的:

图11 Test.cpp演示样例

图11是一个名为Test.cpp的演示样例。在这个演示样例中:

  • 包括Type.h和TypeClass.h。
  • 调用两个函数。当中一个函数是Type.h里声明的test。

    由于test位于my_type命名空间里,所以须要通过my_type::test方式来调用它。

接着来看图12:

图12 Type.cpp

图12所看到的为Type.cpp:

  • 从文件名称上看。Type.cpp和Type.h可能会有些关系。确实如此。正如前文所说。头文件一般做声明用,而真正的实现往往放在源文件里。

    出于文件管理方便性的考虑。头文件和相应的源文件有着同样的文件名称。

  • Type.cpp还包括了iostreamiomanip两个头文件。须要特别注意的是。这两个include使用的是尖括号<>。而不是""。依据约定俗成的习惯,尖括号里的头文件往往是操作系统和C++标准库提供的头文件。包括这些头文件时不用携带.h的后缀。

    比方,#include <iostream>这条语句无需写成#include <iostream.h>

    这是由于C++标准库的实现是由不同厂商来完毕的。

    详细实现的时候可能头文件没有后缀名。或者后缀名不是.h。

    所以,C++规范将这个问题交给编译器来处理。它会依据情况找到正确的文件。

  • C++标准库里的内容都定义在一个独立的命名空间里,这个命名空间叫std。假设须要使用某个命名空间里的东西。比方图12中的代表标准输出对象的cout,能够通过std::cout来訪问它,或者像图12一样。通过using std::cout的方式来避免每次都书写"std::"。

    当然,也能够一次性将某个命名空间里的全部内容全部包括进来,方法就是usingnamespace std。这样的做法和java的import非常相似。

  • my_type命名空间里包括testchangeRef两个函数。当中,test函数实现了Type.h中声明的那个test函数。

    而由于changeRef全然是在Type.cpp中定义的,所以仅仅有Type.cpp内部才知道这个函数,而外界(其它源文件,头文件)不知道这个世界上还有一个changeRef函数。在此请读者注意,一般而言,include指令用于包括头文件。极少用于包括源文件。

  • Type.cpp还定义了一个changeNoRef函数,此函数是在my_type命名空间之外定义的,所以它不属于my_type命名空间。

到此,我们通过几个演示样例向读者展示了C++中头文件和源文件的构成和一些经常使用的代码写法。如今看看怎样编译它们。

1.2.3  编译

C/C++程序通常是通过编写Makefile来编译的。

Makefile事实上就是一个命令的组合,它会依据情况运行不同的命令,包括编译。链接等。Makefile不是C++学习的必备知识点,笔者不拟讨论太多,读者通过图13做简单了解就可以:

图13 Makefile演示样例

图13中,真正的编译工作还是由编译器来完毕的。图13中展示了编译器的工作步骤以及相应的參数。此处笔者仅强调三点:

  • Makefile是一个文件的文件名称。该文件由make命令解析并处理。所以,我们可认为Makefile是专门供make命令使用的脚本文件。

    其内容的书写规则遵守make命令的要求。

  • C++中。编译单元是源文件(即.cpp文件)。如图中①所看到的的内容,编译命令的输入都是xxx.cpp源文件,极少有单独编译.h头文件的。
  • 笔者习惯先编译单个源文件以得到相应的obj文件。然后再链接这些obj文件得到终于的目标文件。链接的步骤也是由编译器来完毕,仅仅只是其输入文件从源文件变成了obj文件。

make命令怎样运行呢?非常easy:

  • 进入到包括Makfile文件的文件夹下。运行make。假设没有指明Makefile文件名称的话,它会以当前文件夹下的Makefile文件为输入。

    make将解析Makefile文件里定义的任务以及它们的依赖关系。然后对任务进行处理。假设没有指明任务名的话,则运行Makefile中定义的第一个任务。

  • 能够通过make任务名来运行Makefile中的指定任务。比方,图13中最后两行定义了clean任务。通过make clean可运行它。clean任务的目标就是删除暂时文件(比方obj文件)和上一次编译得到的目标文件。

提示

Makefile和make是一个独立的知识点,关于它们的故事能够写出一整本书了。

只是,就实际工作而言。开发人员往往会把Makefile写好。或者可借助一些工具以自己主动生成Makefile。

所以。假设读者不了解Makefile的话也不用操心,仅仅要会运行make命令就能够了。

1.3  Class介绍

本节介绍C++中面向对象的核心知识点——类(Class)。笔者对类有三点认识:

  • Class是C++构造面向对象世界的核心单元。面向对象在编码中的直观体现就是程序猿能够用Class封装成员变量和成员函数。曾经用C敲代码的时候,是面向过程的思维方法。考虑的是函数和函数之间的调用和跳转关系。C++出现后。我们看待问题和解决这个问题的思路发生了非常大的变化。很多其它考虑是设计合适的类并处理对象和对象之间的关系。当然,面向对象并非说程序就没有过程了。

    程序总还是有顺序。有流程的。

    可是在这个流程里,开发人员很多其它关注的是对象以及对象之间的交互,而不是孤零零的函数。

  • 另外。Class还支持抽象,继承和多态。这些概念全然就是环绕面向对象来设计和考虑的,它关注的是类和类之间的关系。
  • 最后,从类型的角度来看,和C++基础内置数据类型一样,类也是一种数据类型,仅仅只是它是一种可由开发人员自己定义的数据类型罢了。

探讨:

笔者曾经差点儿没有从类型的角度来看待过类。

直到接触模板编程后,才发现类型和类型推导在模板中的重要作用。关于这个问题,我们留待兴许介绍模板编程时再继续讨论。

以下我们来看看C++中的Class该怎么实现。先来看图14所看到的的TypeClass.h。它声明了一个名为Base的类。请读者重点关注它的语法:

图14  Base类的声明

来看图14的内容:

  • 首先。笔者用classkeyword声明了一个名为Base的类。Base类位于type_class命名空间里。
  • C++类有和Java一样的訪问权限控制。关键词也是publicprivateprotected三种。只是其使用方法和Java略有差别。

    Java中,每个成员(包括函数和变量)都须要单独声明訪问权限,而C++则是分组控制的。比如,位于"public:"之后的成员都有同样的public訪问权限。假设没有指明訪问权限,则默认使用private訪问权限。

  • 在类成员的构成上,C++除了有构造函数赋值函数析构函数等三大类特殊成员函数外,还能够定义其它成员函数和成员变量。成员变量如图14中的size变量能够像Java那样在声明时就赋初值,但笔者感觉C++的习惯做法还是仅仅声明成员变量。然后到构造函数中去赋初值。
  • C++中,函数声明时能够指明參数的默认值,比方deleteC函数,它有三个參数。后面两个參数均有默认值(參数b的默认值是100,參数test的默认值是true)。

接下来,我们先介绍C++的三大类特殊函数。

注意。

这三类特殊函数并非都须要定义。笔者此处列举它们仅为学习用。

1.3.1  构造,赋值和析构函数

C++类的三种特殊成员函数各自是构造、赋值和析构。当中:

  • 构造函数:当创建类的实例对象时。这个对象的构造函数将被调用。一般在构造函数中做该对象的初始化工作。Java中的类也有构造函数,和C++中的构造函数相似。
  • 赋值函数:赋值函数事实上就是指"="号操作符,用于将变量A赋值给同类型(不考虑类型转换等情况)的变量B。比方,能够将整型变量(假设变量名为aInt)的值赋给还有一个整型变量bInt。在此基础上,我们也能够将类A的某个实例(假设变量名为aA)赋值给类A的另外一个实例bA。请读者注意,1.3节一開始就强调过,类仅仅只是是一种自己定义的数据类型罢了。假设整型变量(或者其它基础内置数据类型)能够赋值的话。类也应该支持赋值操作。
  • 析构函数:当对象的生命走向终结时,它的析构函数将被调用。一般而言,该函数内部会释放这个对象占领的各种资源。

    Java中,和析构函数相似的是finalize方法。只是,由于Java实现了内存自己主动回收机制。所以Java程序猿差点儿不须要考虑finalize的事情。

以下,我们分别来讨论这三种特殊函数。

1.  构造函数

来看类Base的构造函数,如图15所看到的:

图15  构造函数演示样例

图15中的代码实现于TypeClass.cpp中:

  • 在类声明之外实现类的成员函数时。须要通过"类名::函数名"的方式告诉编译器这是一个类的成员函数。比方图15中的Base::Base(int a)
  • 默认构造函数:默认构造函数是指不带參数或全部參数全部有默认值的构造函数。注意。C++的函数是支持參数带默认值的,比方图14中Base类的deleteC函数。
  • 普通构造函数:带參数的构造函数。
  • 拷贝构造函数:使用方法如图15中的③所看到的。详情可见下文介绍。

以下来介绍图15中几个值得注意的知识点:

(1)  构造函数初始值列表

构造函数基本的功能是完毕类实例的初始化。也就是对象的成员变量的初始化。C++中。成员变量的初始化推荐使用初始值列表(constructor initialize list)的方法(使用方法如图15所看到的),其语法格式为:

构造函数(...):

    成员变量A(A的初值),成员变量B(B的初值){

...//也能够使用花括号,比方成员变量A{A的初值},成员变量B{B的初值}

}

当然,成员变量的初值设置也能够通过赋值方式来完毕:

构造函数(...){

  成员变量A=A的初值;

  成员变量B=B的初值;

  ....

}

C++中。构造函数中使用初值列表和成员变量赋初值是有差别的,此处不拟详细讨论二者的差异。但推荐使用初值列表的方式,原因大致有二:

  • 使用初值列表可能运行效率上会有提升。
  • 有些场合必须使用初值列表,比方子类构造函数中初始化基类的成员变量时。后文中将看到这样的样例。

提示:

构造函数中请使用初值列表的方式来完毕变量初始化。

(2)  拷贝构造函数

拷贝构造,即从一个已有的对象拷贝其内容,然后构造出一个新的对象。拷贝构造函数的写法必须是:

构造函数(const 类& other)

注意,const是C++中的常量修饰符,与Java的final相似。

拷贝过程中有一个问题须要程序猿特别注意,即成员变量的拷贝方式是值拷贝还是内容拷贝。以Base类的拷贝构造为例。假设新创建的对象名为B,它用已有的对象A进行拷贝构造:

  • memberA和memberB是值拷贝。

    所以,A对象的memberA和memberB将赋给B的memberA和memberB。此后,A、B对象的memberA和memberB值分别同样。

  • 而对pMemberC来说,情况就不一样了。B.pMemberC和A.pMemberC将指向同一块内存。假设A对这块内存进行了操作,B知道吗?更有甚者,假设A删除了这块内存,而B还继续操作它的话,岂不是会崩溃?所以。对于这样的情况,拷贝构造函数中使用了所谓的深拷贝(deepcopy),也就是将A.pMemberC的内容复制到B对象中(B先创建一个大小同样的数组,然后通过memcpy进行内存的内容拷贝),而不是简单的进行赋值(这样的方式叫浅拷贝。shallow copy)。

值拷贝、内容拷贝和浅拷贝、深拷贝

由上述内容可知,浅拷贝相应于值拷贝,而深拷贝相应于内容拷贝。对于非指针变量类型而言。值拷贝和内容拷贝没有差别,但对于指针型变量而言,值拷贝和内容拷贝差别就非常大了。

图16解释了深拷贝和浅拷贝的差别:

图16  浅拷贝和深拷贝的差别

图16中,浅拷贝用红色箭头表示。深拷贝用紫色箭头表示:

  • 浅拷贝最明显的问题就是A和B的pMemberC将指向同一块内存。绝大多数情况下,浅拷贝的结果绝不是程序猿想要的。
  • 採用深拷贝的话。A和B将具有同样的内容,但彼此之间不再有不论什么纠葛。
  • 对于非指针型变量而言。深拷贝和浅拷贝没有什么差别。事实上就是值的拷贝

最后,笔者还要特别说明拷贝构造函数被触发的场合。

来看代码:

Base A; //构造A对象

Base B(A);// ①直接用A对象来构造B对象,这样的情况是“直接初始化”

Base C = A;// ②定义C的时候即赋值。这是真正意义上的拷贝构造。二者的差别见下文介绍。

除了上述两种情况外,还有一些场合也会导致拷贝构造函数被调用,比方:

  • 当函数的參数为非引用的类类型时。调用这个函数并传递实參时。实參的拷贝构造函数被调用。
  • 函数的返回类型为一个非引用的对象时,该对象的拷贝构造函数被调用。

直接初始化和拷贝初始化的细微差别

Base B(A)仅仅是导致拷贝构造函数被调用,但并非严格意义上的拷贝构造,由于:

  1. Base确实定义了一个形參为constB&的构造函数。而B(A)的语法恰好满足这个函数,所以这个构造函数被调用是理所当然的。这样的构造是非常直接的。没有不论什么疑义的,所以叫直接初始化。
  2. 而对于Base C = A的理解却是将A的内容复制到正在创建的C对象中,这里包括了拷贝和构造两个概念,即拷贝A的内容来构造C。所以叫拷贝构造。羞愧得说,笔者也非常难描写叙述上述内容在语法上的精确含义。只是。从使用角度来看,读者仅仅需记住这两种情况均会导致拷贝构造函数被调用就可以。

2.  拷贝赋值函数

拷贝赋值函数是赋值函数的一种。我们先来思考下赋值函数解决什么问题。请读者思考以下这段代码:

int a = 0;

int b = a;//将a赋值给b

全部读者应该对上述代码都不会有不论什么疑问。是的,对于基本内置数据类型而言,赋值操作似乎是天经地义的合理。但对于类类型呢?比方以下的代码:

Base A;//构造一个对象A

Base B; //构造一个对象B

B = A; //①A能够赋值给B吗?

从类型的角度来看。没有理由不同意类这样的自己定义数据类型的进行赋值操作。

可是从面向对象角度来看,把一个对象赋值给另外一个对象会得到什么?现实生活中似乎也难以到相似的场景来比拟它。

无论怎样,C++是支持一个对象赋值给还有一个对象的。

如今把注意力回归到拷贝赋值上来。来看图17所看到的的代码:

图17  拷贝赋值函数演示样例

赋值函数本身没有什么难度。无非就是在准备接受另外一个对象的内容前。先把自己清理干净。另外,赋值函数的关键知识点是利用了C++中的操作符重载(Java不支持操作符重载)。关于操作符重载的知识请读者阅读本文兴许章节。

3.  移动构造和移动赋值函数

前面两节介绍了拷贝构造和拷贝赋值函数,还了解了深拷贝和浅拷贝的差别。

但关于构造和赋值的故事并没有完。由于C++11中,除了拷贝构造和拷贝赋值之外,还有移动构造和移动赋值。

注意

这几个名词中:构造和赋值并没有变。变化的是构造和赋值的方法。前2节介绍的是拷贝之法,本节来看移动之法。

(1)  移动之法的解释

图18展示了移动的含义:

图18  Move的示意

对照图16和图18,读者会发现移动的含义事实上非常easy。就是把A对象的内容移动到B对象中去:

  • 对于memberA和memberB而言,由于它们是非指针类型的变量。移动和拷贝没有不同。
  • 但对于pMemberC而言。差别就非常大了。

    假设使用拷贝之法。A和B对象将各自有一块内存。

    假设使用移动之法,A对象将不再拥有这块内存。反而是B对象拥有A对象之前拥有的那块内存。

移动的含义好像不是非常难。只是,让我们更进一步思考一个问题:移动之后,A、B对象的命运会发生怎样的改变?

  • 非常easy。B自然是得到A的全部内容。
  • A则掏空自己,成为无用之物。

    注意,A对象还存在。可是你最好不要碰它,由于它的内容早已经移交给了B。

移动之后。A竟然无用了。什么场合会须要如此“残忍”的做法?还是让我们用演示样例来阐述C++11推出移动之法的目的吧:

图19  有Move和没有Move的差别

图19中,左上角是演示样例代码:

  • test函数:将getTemporyBase函数的返回值赋给一个名为a的Base实例。
  • getTemporyBase函数:构造一个Base对象tmp并返回它。

图19展示了未定义移动构造函数和定义了移动构造函数时该程序运行后打印的日志。同一时候图中还解释了运行的过程。结合前文所述内容,我们发现tmp确实是一种转移出去(无论是採用移动还是拷贝)后就不须要再使用的对象了。对于这样的情况,移动构造所带来的优点是显而易见的。

注意:

对于图中的測试函数,如今的编译器已经能做到高度优化,以至于图中列出的移动或拷贝调用都不须要了。

为了达到图中的效果,编译时必须加上-fno-elide-constructors标志以禁止这样的优化。读者最好还是一试。

以下,我们来看看代码中是怎样体现移动的。

(2)  移动之法的代码实现和左右值介绍

图20所看到的为Base的移动构造和移动赋值函数:

图20  移动构造和移动赋值演示样例

图20中,请读者特别注意Base类移动构造和移动赋值函数的參数的类型,它是Base&&。没错,是两个&&符号:

  • 假设是Base&&(两个&&符号),则表示是Base的右值引用类型。
  • 假设是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是有名的。所以它不再是右值。

假设读者想了解很多其它关于左右值的差别,请阅读本章所列的參考书籍。此处笔者再强调一下移动构造和赋值函数在什么场合下使用的问题,请读者注意把握两个关键点:

  • 第一,假设确定被转移的对象(比方图19中的tmp对象)不再使用,就能够使用移动构造/赋值函数来提升运行效率。
  • 第二,我们要保证移动构造/赋值函数被调用。而不是拷贝构造/赋值函数被调用。比如。上述代码中Base y = x这段代码实际上触发了拷贝构造函数,这不是我们想要的。

    为此。我们须要强制使用移动构造函数,方法为Base y = std::move(x)move是std标准库提供的函数,用于将參数类型强制转换为相应的右值类型。通过move函数。我们表达了强制使用移动函数的想法。

假设未定义移动函数怎么办?

假设类未定义移动构造或移动赋值函数,编译器会调用相应的拷贝构造或拷贝赋值函数。所以,使用std::move不会带来什么副作用。它仅仅是表达了要使用移动之法的愿望。

4.  析构函数

最后,来看类中最后一类特殊函数。即析构函数。

当类的实例达到生命终点时,析构函数将被调用,其主要目的是为了清理该实例占领的资源。

图21所看到的为Base类的析构函数演示样例:

图21  析构函数演示样例

Java中与析构函数相似的是finalize函数。

但绝大多数情况下。Java程序猿不用关心它。而C++中,我们须要知道析构函数什么时候会被调用:

² 栈上创建的类实例。在退出作用域(比方函数返回,或者离开花括号包围起来的某个作用域)之前,该实例会被析构。

² 动态创建的实例(通过new操作符)。当delete该对象时,其析构函数会被调用。

1.  总结

1.3.1节介绍了C++中一个普通类的大致组成元素和当中一些特殊的成员函数,比方:

  • 构造函数,分为默认构造,普通构造,拷贝构造和移动构造。
  • 赋值函数,分为拷贝赋值和移动赋值。

    请读者先从原理上理解拷贝和移动的差别和它们的目的。

  • 析构函数。

1.3.2  类的派生和继承

C++中与类的派生、继承相关的知识比較复杂,相对琐碎。本节中,笔者拟将精力放在一些相对基础的内容上。

先来看一个派生和继承的样例。如图22所看到的:

图22  派生和继承演示样例

图22中:

  • 右边居中方框①定义了一个Base类。它和图14中的内容一样。
  • 右下方框②定义了一个VirtualBase类。它包括构造函数,虚析构函数。虚函数test1,纯虚函数test2和一个普通函数test3。
  • 左边方框③定义了一个Derived类,它同一时候从Base和VirtualBase类派生,属于多重继承。
  • 图中给出了10个须要读者注意的函数和它们的简介。

和Java比較

Java中尽管没有类的多重继承。但一个类能够实现多个接口(Interface),这事实上也算是多重继承了。

相比Java的这样的设计,笔者认为C++中类的多重继承太过灵活。使用时须要特别小心,否则菱形继承的问题非常难避免。

如今,先来看一下C++中派生类的写法。如图22所看到的,Derived类继承关系的语法例如以下:

class  Derived:private Base,publicVirtualBase{

}

当中:

  • classDerived之后的冒号是派生列表。也就是基类列表,基类之间用逗号隔开。
  • 派生有publicprivateprotected三种方式。其意义和Java中的类派生方式几乎相同,大抵都是用于控制派生类有何种权限来訪问继承得到的基类成员变量和成员函数。注意,假设没有指定派生方式的话,默认为private方式。

了解C++中怎样编写派生类后,下一步要关注面向对象中两个重要特性——多态和抽象是怎样在C++中体现的。

注意:

笔者此处所说的抽象是狭义的。和语言相关的,比方Java中的抽象类。

1.  虚函数、纯虚函数和虚析构函数

Java语言里,多态是借助派生类重写(override)基类的函数来表达,而抽象则是借助抽象类(包括抽象方法)或者接口来实现。

而在C++中,虚函数纯虚函数就是用于描写叙述多态和抽象的利器:

  • 虚函数:基类定义虚函数。派生类能够重写(override)它。当我们拥有一个派生类对象。但却是通过基类引用类型或者基类指针类型的变量来调用该对象的虚函数时,被调用的虚函数是派生类重写过的虚函数(假设该虚函数被派生类重写了的话)。
  • 纯虚函数:拥有纯虚函数的类不能实例化。

    从这一点看,它和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的时候。基类和派生类声明的虚函数在函数名。參数等信息上需保持一致。

    但对析构函数而言,由于析构函数的函数名必须是"~类名"。所以派生类和基类的析构函数名肯定是不同的。

  • 可是。我们又希望多态对于析构函数(注意,析构函数也是函数。和普通函数没什么差别)也是可行的。

    比方,当通过基类指针来删除派生类对象时。是派生类对象的析构函数被调用。所以,当基类中假设有虚函数时候。一定要记得将其析构函数变成虚析构函数。

阻止虚函数被override

C++中,也能够阻止某个虚函数被override。方法和Java相似,就是在函数声明后加入final关键词。比方

virtual void test1(boolean test) final;//如此,test1将不能被派生类override了

最后,我们通过一段演示样例代码来加深对虚函数的认识。如图23所看到的:

图23  虚函数測试演示样例

图23是笔者编写的一个非常easy的样例。左边是代码,右边是运行结果。简而言之:

  • 假设想实现多态,就在基类中为须要多态的函数添加virtual关键词。
  • 假设基类中有虚函数,也请同一时候为基类的析构函数加入virtual关键词。

    仅仅有这样,指向派生类对象的基类指针变量被delete时,派生类的析构函数才干被调用。

提示:

1 请读者尝试改动測试代码,然后观察打印结果。

2 读者可将图23中代码的最后一行改写成pvb->~VirtualBase(),即直接调用基类的析构函数,但由于它是虚析构函数,所以运行时。~Derived()将先被调用。

2.  构造和析构函数的调用次序

类的构造函数在类实例被创建时调用,而析构函数在该实例被销毁时调用。假设该类有派生关系的话。其基类的构造函数和析构函数也将被依次调用到,那么,这个依次的顺序是什么?

  • 对构造函数而言。基类的构造函数先于派生类构造函数被调用。假设派生类有多个基类,则基类依照它们在派生列表里的顺序调用各自的构造函数。比方Derived派生列表中基类的顺序是:先Base,然后是VirtualBase。

    所以Base的构造函数先于VirtualBase调用,最后才是Derived的构造函数。

  • 析构函数则是相反的过程,即派生类析构函数先被调用,然后再调用基类的析构函数。

    假设是多重继承的话,基类依照它们在派生列表里出现的相反次序调用各自的析构函数。

    比方Derived类实例析构时。Derived析构函数先调用,然后VirtualBase析构,最后才是Base的析构。

补充内容:

假设派生类含有类类型的成员变量时,调用次序将变成:

构造函数:基类构造->派生类中类类型成员变量构造->派生类构造

析构函数:派生类析构->派生类中类类型成员变量析构->基类析构

多重派生的话,基类依照派生列表的顺序/反序构造或析构

3.  编译器合成的函数

Java中。假设程序猿没有为类编写构造函数函数。则编译器会为类隐式创建一个不带不论什么參数的构造函数。这样的编译器隐式创建一些函数的行为在C++中也存在。仅仅只是C++中的类有构造函数,赋值函数,析构函数,所以情况会复杂一些,图24描写叙述了编译器合成特殊函数的规则:

图24  编译器合成特殊函数的规则

图24的规矩可简单总结为:

  •  假设程序猿定义了不论什么一种类型的构造函数(拷贝构造、移动构造。默认构造,普通构造)。则编译器将不再隐式创建默认构造函数
  • 假设程序未定义拷贝(拷贝赋值或拷贝构造)函数或析构函数,则编译器将隐式合成相应的函数。
  • 假设程序未定义移动(移动赋值或移动构造)函数,而且,程序未定义析构函数或拷贝函数(拷贝构造和拷贝赋值)。则编译器将合成相应的移动函数。

从上面的描写叙述可知,C++中编译器合成特殊函数的规则是比較复杂的。即使如此。图24中展示的规则还仅是冰山一角。以移动函数的合成而言。即使图中的条件满足。编译器也未必能合成移动函数,比方类中有无法移动的成员变量时。

关于编译器合成规则,笔者个人感觉开发人员应该以实际需求为出发点,假设确实须要移动函数,则在类声明中定义就可以。

(1)  =default和=delete

有些时候我们须要一种方法来控制编译器这样的自己主动合成的行为,控制的目的无外乎两个:

  • 让编译器必须合成某些函数。
  • 禁止编译器合成某些函数。

借助=default=delete标识,这两个目的非常easy达到,来看一段代码:

//定义了一个普通的构造函数。但同一时候也想让编译器合成默认的构造函数。则能够使用=default标识

Base(int x); //定义一个普通构造函数后,编译器将停止自己主动合成默认的构造函数

//=default后。强制编译器合成默认的构造函数。注意。开发人员不用实现该函数

Base() = default;//通知编译器来合成这个默认的构造函数

//假设不想让编译器合成某些函数,则使用= delete标识

Base&operator=(const Base& other) = delete;//阻止编译合成拷贝赋值函数

注意,这样的控制行为仅仅针对于构造、赋值和析构等三类特殊的函数。

(2)  “继承”基类的构造函数

一般而言,派生类可能希望有着和基类相似的构造方法。比方。图25所看到的的Base类有3种普通构造方法。如今我们希望Derived也能支持通过这三种方式来创建Derived类实例。怎么办?图25展示了两种方法:

图25  派生类“继承”基类构造函数

  • 第一种方法就是在Derived派生类中手动编写三个构造函数。这三个构造函数和Base类里的一样。
  • 第二种方法就是通过使用using关键词“继承”基类的那三个构造函数。

    继承之后。编译器会自己主动合成相应的构造函数。

注意,这样的“继承”事实上是一种编译器自己主动合成的规则。它仅支持合成普通的构造函数。而默认构造函数,移动构造函数。拷贝构造函数等遵循正常的规则来合成。

探讨

前述内容中。我们向读者展示了C++中编译器合成一些特殊函数的做法和规则。实际上,编译器合成的规则比本节所述内容要复杂得多。建议感兴趣的读者阅读參考文献来开展进一步的学习。

另外,实际使用过程中,开发人员不能全然依赖于编译器的自己主动合成。有些细节问题必须由开发人员自己先回答。比方。拷贝构造时,我们须要深拷贝还是浅拷贝?需不须要支持移动操作?在获得这些问题答案的基础上,读者再结合编译器合成的规则,然后才选择由编译器来合成这些函数还是由开发人员自己来编写它们。

1.3.3  友元和类的前向声明

前面我们提到过,C++中的类訪问事实上例的成员变量或成员函数的权限控制上有着和Java相似的关键词,如publicprivateprotected。严格遵守“信息该公开的要公开,不该公开的一定不公开”这一封装的最高原则无疑是一件好事。但现实生活中的情况是如此变化万端,有时候我们也须要破个例。

比方。熟人之间能否够公开一些信息以避开假设按“公事公办”走流程所带来的过高沟通成本的问题?

C++中,借助友元,我们能够做到小范围的公开信息以降低沟通成本。从编程角度来看,友元的作用无非是:提供一种方式,使得类外某些函数或者某些类能够訪问一个类的私有成员变量或成员函数。

对被訪问的类而言,这些类外函数或类,就是被訪问的类的朋友

来看友元的演示样例。如图26所看到的:

图26  类的友元示意

图26展示了怎样为某个类指定它的“朋友们”,C++中,类的友元能够是:

  • 一个类外的函数或者一个类中的某些成员函数。

    假设友元是函数。则必须指定该函数的完整信息,包括返回值,參数,属于哪个类等。

  • 一个类。

基类的友元会变成从该基类派生得来的派生类的友元吗?

C++中,友元关系不能继承,也就是说:

1 基类的友元能够訪问基类非公开成员。也能訪问派生类中属于基类的非公开成员。

2 可是不能訪问派生类自己定义的非公开成员。

友元比較简单,此处就不拟多说。如今我们介绍下图26中提到的类的前向声明,先来回想下代码:

class Obj;//类的前向声明

void accessObj(Obj& obj);

C++中。数据类型应该先声明,然后再使用。

但这会带来一个“先有鸡还是先有蛋”的问题:

  • accessObj函数的參数中用到了Obj。

    可是类Obj的声明却放在图26的最后。

  • 假设把Obj的声明放在accessObj函数的前面,这又无法把accessObj指定为Obj的友元。由于友元必须要指定完整的函数。

怎么破解这个问题?这就用到了类的前向声明,以图26为例,Obj前向声明的目的就是告诉类型系统。Obj是一个class。不要把它当做别的什么东西。

一般而言,类的前向声明的使用方法例如以下:

  • 假设头文件b.h中须要引入a.h头文件里定义的类A

    可是我们不想在b.h里包括a.h。由于a.h可能太复杂了。

    假设b.h里包括a.h,那么全部包括b.h的地方都间接包括了a.h。此时。通过引入A的前向声明,b.h中能够使用类A。

  • 注意,类的前向声明一种声明,真正使用的时候还得包括类A所在的头文件a.h。

    比方,b.cpp(b.h相相应的源文件)是真正使用该前向声明类的地方。那么仅仅要在b.cpp里包括a.h就可以。

这就是类的前向声明的使用方法,即在头文件里进行类的前向声明,在源文件里去包括该类的头文件。

类的前向声明的局限

前向声明优点非常多,但同一时候也有限制。以Obj为例,在看到Obj完整定义之前,不能声明Obj类型的变量(包括类的成员变量),可是能够定义Obj引用类型或Obj指针类型的变量。比方,你无法在图26中class Obj类代码之前定义ObjaObj这样的变量。

仅仅能定义Obj& refObjObj* pObj。之所以有这个限制,是由于定义Obj类型变量的时候,编译器必须确定该变量的大小以分配内存,由于没有见到Obj的完整定义。所以编译器无法确定其大小,但引用或者指针则不存在此问题。

读者最好还是一试。

1.3.4  explicit构造函数

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;

}

1.3.5  C++中的struct

struct是C语言中的古老成员了,在C中它叫结构体。只是到了C++世界。struct不再是C语言中结构体了。它升级成了class。即C++中的struct就是一种class,它拥有类的全部特征。只是,struct和普通class也有一点差别。那就是struct的成员(包括函数和变量)默认都是public的訪问权限。

1.4  操作符重载

对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++中操作符重载的实现方式。

1.4.1  操作符重载的实现方式

操作符重载说白了就是将操作符当成函数来对待。当运行某个操作符运算时。相应的操作符函数被调用。和普通函数比起来,操作符相应的函数名由“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类型”的推导是非常自然的。上述演示样例中。笔者有意展示了操作符重载的灵活性。故而重载了三个+操作符函数。

1.4.2  输出和输入操作符重载

本章非常多演示样例代码都用到了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的目的是将类实例的内容转换成字符串以方便打印或者别的用途。

1.4.3  ->和*操作符重载

->*操作符重载一般用于所谓的智能指针类,它们必须实现为类的成员函数。

在介绍相关演示样例代码前。笔者要特别说明一点:这两个操作符假设操作的是指针类型的对象,则并非重载,比方以下的代码:

//假设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对象)所占领的资源都能够完美回收。

1.4.4  new和delete操作符重载

new和delete操作符的重载与其它操作符的重载略有不同。寻常我们所说的new和delete实际上是指new表达式(expression)以及delete表达式,比方:

Object* pObject =new Object; //new表达式,对于数组而言就是new Object[n];

deletepObject;//delete表达式,对于数组而言就是delete[] pObject

上面这两行代码各自是new表达式和delete表达式。这两个表达式是不能自己定义的,可是:

² new表达式运行过程中将首先调用operator new函数。而C++同意程序猿自己定义operatornew函数。

² delete表达式运行过程的最后将调用operator delete函数,而程序猿也能够自己定义operatordelete函数。

所以。所谓new和delete的重载实际上是指operator new和operator delete函数的重载。

以下我们来看一下operator new和operator delete函数怎样重载。

提示:

为行文方便。下文所指的new操作符就是指operator new函数。delete操作符就是指operator delete函数。

1.  new和delete操作符语法

我们先来看new操作符的语法。如图32所看到的:

图32  new的语法

new操作符一共同拥有12种形式。使用方法相当灵活。当中:

  • 程序猿能够重载(1)到(4)这四个函数。这四个函数是全局的。即它们不属于类的成员函数。

    有些new函数会抛异常,只是笔者接触的程序中都没有使用过C++中的异常,所以本书不拟讨论它们。

  • (5)到(8)为placement new系列函数。placement new事实上就是给new操作符提供除内存大小之外(即count參数)的别的參数。比方“new(2,f)T”这样的表达式将相应调用operatornew(sizeof(T), 2, f)函数,注意,这几个函数也是系统全局定义的。另外,C++规定(5)和(6)这两个函数不同意全局重载。
  • (9)到(12)定义为类的成员函数。注意,尽管上边的(5)和(6)不能进行全局重载。可是在类中却能够重载它们。

请读者务必注意。假设我们在类中重载了随意一种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还要复杂。

此处须要特别说明的是:

  • new表达式能够带參数,比方new(2,f)T。
  • 但delete表达式不能传递參数。所以像图33中带參数的delete操作符函数,比方(7)到(10),(15)、(16)这几个函数将怎样调用呢?C++规范里说,当使用相应形式的new操作符构造一个或一组类实例时,假设当中有一个实例的构造函数抛出异常,那么相应形式的delete操作符函数将被调用。

上面的描写叙述不太直观。我们通过一个样例进一步来解释它。如图34所看到的:

图34 delete操作符的使用方法演示样例

图34中:

  • 类X的构造函数抛出一个异常。
  • 类X重载了一个new操作符和一个delete操作符。

    这两个操作符函数最后一个參数都是bool型。

  • main函数中。使用placementnew表达式触发了类X的new操作符被调用。
  • 由于X构造函数抛出异常,所以系统会调用X重载的delete函数,也就是最后一个參数是bool的那个delete函数。

图34中还特别指出代码中不能直接使用delete p1这样的表达式,这会导致编译错误。提示没有匹配的delete函数,这是由于:

  • 类重载的delete函数有參数。这个函数仅仅能在类实例构造时抛出异常时调用。而类X未定义如图33中(11)或(13)所看到的的delete函数。
  • 而且,类仅仅要重定义不论什么一个delete函数,这都将导致系统的delete函数被隐藏。

提示:

关于全局delete函数被隐藏的问题。读者最好还是动手一试。

2.  new和delete操作符重载演示样例

如今我们来看new和delete操作符重载的一个简单演示样例。

如图35所看到的:

强调:

考虑到new和delete的高度灵活性以及和它们和内存分配释放紧密相关的重要性,程序猿最好仅仅针对特定类进行new和delete操作符的重载。

图35 new/delete操作符重载的演示样例

图35中。笔者为Obj重载了两个new操作符和两个delete操作符:

  • 当像測试代码中那样创建Obj实例时,这两个new操作符重载函数分别会被调用。
  • delete函数略有特殊。它存在优先级的问题。第一个delete函数优先级高于第二个delete函数。假设第一个delete函数被凝视,那么第二个delete函数将被调用。

讨论:重载new和delete操作符的优点

通过重载new和delete操作符,我们有机会在对象创建和释放的时候做一些内存管理的工作。

比方,每次new一个Obj对象。我们递增new被调用的次数。delete的时候再递减。当程序退出时。我们检查该次数是否归0。

假设不为0,则表示有Obj对象没有被delete,这非常可能就是内存泄露的潜在原因。

3.  怎样在指定内存中构造对象

我们用new表达式创建一个对象的时候,系统将在堆上分配一块内存。然后这个对象在这块内存上被构造。由于这块内存分配在堆上,程序猿一般无法指定其地址。

这一点和Java中的new相似。但有时候我们希望在指定内存上创建对象,能够做到吗?对于C++这样的灵活度非常高的语言而言,这个小小要求自然能够轻松满足。仅仅要使用特殊的new就可以:

²  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

1.4.5  函数调用运算符重载

函数调用运算符使得对象能像函数一样被调用,什么意思呢?我们知道C++和Java一样,函数调用的写法是“函数名(參数)”。假设我们把函数名换成某个类的对象,即“对象(參数)”。就达到了对象像函数一样被调用的目的。

这个过程得以顺利实施的原因是C++支持函数调用运算符的重载,函数调用运算符就是“()”。

来看一个样例,如图37所看到的:

图37 operator ()重载演示样例

图37展示了operator ()重载的演示样例:

² 此操作符的重载比較简单,就和定义函数一样能够依据须要定义參数和返回值。

² 函数调用操作符重载后。Obj类的实例对象就能够像函数一样被调用了。我们一般将这样的能像函数一样被调用的对象叫做函数对象。图37也提到。普通函数是没有状态的。可是函数对象却不一样。函数对象首先是对象,然后才是能够像函数一样被调用。

而对象是有所谓的“状态”的,比方图中的obj和obj1,两个对象的mX取值不同。这将导致外界传入一样的參数却得到不同的调用结果。

1.5  函数模板与类模板

模板是C++语言中比較高级的一个话题。

羞愧得讲,笔者使用C++、Java这么些年。极少自己定义模板,最多就是在使用容器类的时候会接触它们。由于日常工作中用得非常少,所以对它的认识并不深刻。

这一次由于ART代码中大量使用了模板,所以笔者也算是被逼上梁山。从头到尾细致研究了C++中的模板。

介绍模板详细知识之前,笔者先分享几点关于模板的非常重要的学习心得:

  • C++是面向对象的语言。

    面向对象最重要的一个特点就是抽象,即将公共的属性、公共的行为抽象到基类中去。

    这样的抽象非常好理解,现实生活中也无处不在。反观模板,它事实上也是一种抽象,仅仅只是这样的抽象的关注点不在属性,不在行为,而在于数据类型。

    比方。有一个返回两个操作数相加之和的函数,它即能够处理int型操作数,也能够处理long型操作数。

    那么。从数据类型的角度进行抽象的话,我们能够用一个代表通用数据类型的T做为该函数的參数类型。该函数内部仅仅对T类型的变量进行相加。至于T详细是什么,此时不用考虑。

    而使用这个函数的时候。当传入int型变量时,T就变成int。

    当传入long型变量时,T就变成long。

    所以,模板的重点在于将它所操作的数据的类型抽象出来

  • C++是强类型的语言,即全部变量(包括函数參数,返回值)都须要有一个明白的类型。这个要求对于模板这样的基于数据类型的抽象方式有重大和直接的影响。对于模板而言,定义函数模板或类模板时所用的数据类型仅仅是一个标示。比方前面提到的T。而真正的数据类型仅仅有等使用者用详细的数据类型来使用模板时才干确定。相比非模板编程,模板编程多了一个非常关键的步骤,即模板实例化(英文叫instantiation)。

    模板实例化是编译器发现使用者用详细的数据类型来使用模板时,它就会将模板里的通用数据类型替换成详细的数据类型,从而生成实际的函数或类。

    比方前面提到的两个操作数相加的模板函数。当传入int型变量时,模板会实例化出一个參数为int型的函数,当传入long型变量时,模板又会实例化出一个參数为long型的函数。

    当然,假设没有地方用详细数据类型来使用这个模板。则编译器不会生成不论什么函数。注意,模板的实例化是由编译器来做的。但触发实例化的原因是由于使用者用详细数据类型来使用了某个模板。

简而言之。对于模板而言,程序猿须要重点关注两个事情,一个是对数据类型进行抽象,还有一个是利用详细数据类型来绑定某个模板以将事实上例化。

好了,让我们正式进入模板的世界,故事先从简单的函数模板開始。

提示:

模板编程是C++中非常难的部分,參考文献[4]用了六章来介绍与之相关的知识点。

无论怎样,模板的核心依旧是笔者前面提到的两点,一个是数据类型抽象,一个是实例化。

1.5.1  函数模板

1.  函数模板的定义

先来看函数模板的定义方法,如图38所看到的:

图38  函数模板的定义

图38所看到的为两个函数模板的定义,当中有几点须要读者注意:

  • 函数模板一般在头文件里定义,这和普通函数不太一样。普通函数一般在头文件里声明。在源文件里定义。对函数模板而言,由于编译器在实例化一个模板的时候须要知道函数模板的全部内容(再次强调。实例化就是编译器用详细数据类型套用到模板上去。然后生成详细函数的过程),所以实例化过程中仅仅知道函数模板的声明是不够的。更进一步得说。事实上函数模板并非真正的函数,仅仅有编译器用详细数据类型套用到函数模板时才会生成实际的函数。
  • 模板的关键词是template。其后通过<>符号包括一个或多个模板參数。

    模板參数列表不能为空。模板參数和函数參数有些相似,能够定义默认值。

    比方图中add123最后一个模板參数T3。其默认值是long。

提示:

图38中的函数模板定义中,template能够和其后的代码位于同一行。比方:

template<typename T> T add(const T&a1,const T& a2);

建议开发人员将其分成两行,由于这样的代码阅读起来会更easy一些。

以下继续讨论template和模板參数:

首先。能够定义随意多个模板參数。模板參数也能够像函数參数那样有默认值。

其次,函数的參数都有数据类型。相似,模板參数(如上面的T)也有类型之分:

² 代表数据类型的模板參数:用typename关键词标示,表示该參数代表数据类型,实例化时应传入详细的数据类型。比方typename T是一个代表数据类型的模板參数,实例化的时候必须用数据类型来替代T(或者说。T的取值为数据类型。比方int,long之类的)。另外。typename关键词也能够用class关键词替代。所以"template<class T>"和"template<typenameT>"等价。建议读者尽量使用typename作为关键词。

² 非数据类型參数:非数据类型的參数支持整型、指针(包括函数指针)、引用。可是这些參数的值必须在实例化期间(也就是编译期)就能确定。

关于非类型參数,此处先展示一个简单的演示样例,兴许介绍类模板时会碰到详细使用方法。

//以下这段代码中。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函数

}

2.  函数模板的实例化

图39所看到的为图38所定义的两个函数模板的实例化演示样例:

图39  函数模板的实例化

图39所看到的为add和add123这两个函数模板的实例化示意。结合前文反复强调的内容。函数模板的实例化就是当程序用详细数据类型来使用函数模板时,编译器将生成详细的函数:

  • 比方①,编译器依据传入的函数实參推导出数据类型为T,从而会生成一个"intadd(const int &b,const int &b)"函数。终于调用的也是这个生成的函数。

    这是编译器依据函数实參自己主动推导出来的。叫模板实參推导

    推导过程有一些规则,属于比較高级的话题。笔者不拟讨论。只是,不论推导规则有多复杂,其目的就是为了确定模板參数的详细取值情况。这一点请读者牢记。

  • 使用者也能够显示实例化,即显示指明模板參数的类型。

    比方②中所看到的的三个函数。

    编译器将生成三个不同的add123函数。

  • add123函数模板也能够隐式实例化,比方③所看到的。但请读者注意,模板实參的推导仅仅能依据传入的函数參数来确定,不能依据函数的返回值来确定。

    假设add123函数模板中没有为T3设置默认类型的话。编译将出错。

3.  函数模板的特例化

上文介绍了函数模板的实例化,实例化就是指编译器进行类型推导,然后得到详细的函数。

实例化得到的这些函数除了数据类型不一样之外,函数内部的功能是全然一样的。有没有可能为某些特定的数据类型提供不一样的函数功能?

显然,C++是支持这样的做法的,这也被称为模板的特例化(英文简称specialization)。特例化就是当函数模板不太适合某些特定数据类型时。我们单独为它指定一套代码实现。

读者可能会认为非常奇怪,为什么会有这样的需求?以图38中的add123为例,假设程序猿传入的參数类型是指针的话,显然我们不能直接使用add123原函数模板的内容(那样就变成了两个指针值的相加),而应该单独实现一个针对指针类型的函数实现。要达到这个目的就须要用到特例化了。来看详细的做法。如图40所看到的:

图40 特例化演示样例

1.5.2  类模板

1.  类模板定义和特例化

类模板的规则比函数模板要复杂,我们来看一个样例,如图41所看到的:

图41  类模板演示样例

图41中定义一个类模板。其语法格式和函数模板类型,classkeyword前须要由template<模板參数>来修饰。另外,类模板中能够包括普通的成员函数,也能够有成员模板。

这导致类模板的复杂度(包括程序猿阅读代码的难度)大大添加。

注意:

普通类也能包括成员模板,这和函数模板相似,此处不拟详述。

接着来看类模板的特例化,它分为全特化和偏特化两种情况,如图42所看到的:

图42  类模板的全特化和偏特化

图42展示了类模板的全特化和偏特化,当中:

  • 全特化和前文介绍的函数模板的特例化相似,即全部模板參数都指定详细类型或值。全特化类模板得到的是一个实例化的类。
  • 偏特化就是为模板參数中的几个參数指定详细类型或值,剩下的模板參数依旧由使用者来指定。注意,偏特化一个类模板得到的依旧是类模板。

偏特化也叫部分特例化(partial specialization)。

但笔者认为“部分特例化”有些言不尽意。由于偏特化不仅仅包括“为部分模板參数指定详细类型”这一种情况,它还能够为模板參数指定某些特殊类型,比方:

template<typename T> class Test{}//定义类模板Test。包括一个模板參数

//偏特化Test类模板。模板參数类型变成了T*。这就是偏特化的第二种表现形式

template<typename T> class<T*> Test{}

2.  类模板的使用

类模板的使用如图43所看到的:

图43  类模板使用演示样例

图43展示了类模式的使用演示样例。

当中,值得关注的是C++11中程序猿可通过using关键词定义类模板的别名。

而且,使用类模板别名的时候能够指定一个或多个模板參数。

最后,类模板的成员函数也能够在类外(即源文件)中定义,只是这会导致代码有些难阅读,图44展示了怎样在类外定义accessObj和compare函数:

图44  在源文件里定义类模板中的成员函数

图44中:

  • 源文件里定义类模板的成员函数时须要携带类模板的模板參数信息。假设成员函数又是函数模板的话,还得加上函数模板的模板參数信息。这些模板信息放在一起非常easy让代码阅读者头晕。
  • 类模板全特化后得到是详细的类,所以它的成员函数前不须要template关键词来修饰。
  • 类模板成员函数内部假设须要定义该类模板类型的变量时,仅仅需使用类名,而不须要再携带模板信息了。

最后,关于类模板还有非常多知识。比方友元、继承等在类模板中的使用。本书对于这些内容就不拟一一道来,读者以后可在碰到它们的时候再去了解。

1.6  lambda表达式

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

我们在“函数调用运算符重载”一节中曾介绍过函数对象,函数对象是那些重载了函数调用操作符的类的实例。和普通函数比起来:

  • 函数对象首先是一个对象。所以它能够通过成员变量来记录状态。保存信息。
  • 然后,函数对象能够被运行。

通过上面的描写叙述,我们知道函数对象的两个特点,一个是能够保存状态,另外一个是能够运行。

只是,和函数一样,程序猿要使用函数对象的话,首先要定义相应的类。然后才干创建该类的实例并使用它们。

如今我们来思考这样一个问题,可不能够不定义类,而是直接创建某种东西,然后能够运行它们?

  • Java中有匿名内部类能够做到相似的效果。但Java中的类无法重载函数调用操作符,所以匿名内部类不能像函数调用那样运行。
  • Java的匿名内部类给了C++一个非常好的启发,由于C++是支持重载函数调用操作符的,假设我们能在C++中定义匿名函数对象,就能达到所要求的目标了。

以上问题的讨论就引出了C++中的lambda表达式,规范中没有明白说明lambda表达式是什么,但实际上它就是匿名函数对象。以下的代码展示了创建一个lambda表达式的语法结构:

auto f = [ 捕获列表,英文叫capture list ] ( 函数參数 ) ->返回值类型 { 函数体 }

当中:

  • =号右边是lambda表达式,左边是变量定义,变量名为f。lambda表达式创建之后将得到一个匿名函数对象,规范中并没有明白说明这个对象的详细数据类型是什么。所以一般用auto来表示它的类型。注意,auto并非类型名,它仅表示把详细类型的推导交给编译器来做。简而言之,lambda表达式得到的这个匿名对象是有类型的,可是类型叫什么不知道,所以程序猿仅仅好用auto来表示它的类型。反正它的详细类型会由编译器在编译时推导出来[⑨]
  • 捕获列表:lambda表达式一般在函数内部创建。它要捕获的东西也就是函数内能訪问的变量(比方函数的參数,在lambda表达式创建之前所定义的变量。全局变量等)。之所以要捕获它们是由于这些变量代表了lambda创建时所相应的上下文信息,而lambda表达式运行的时候非常可能要利用这些信息。所以。捕获这个词的使用是非常传神的。
  • 函数參数、返回值类型以及函数体:这和普通函数的定义一样。只是。lambda表达式必须使用尾置形式的函数返回声明。

    尾置形式的函数返回声明即是把原来位于函数參数左側的返回值类型放到函数參数的右側。比方,"int func(int a){...} "的尾置声明形式为"autofunc(int a ) -> int {...}"。

    当中,auto是关键词。用在此处表明该函数将採用尾置形式的函数返回声明。

以下我们通过样例进一步来认识lambda表达式,来看图45:

图45 lambda表达式演示样例(1)

图45 lambda表达式演示样例(2)

图45展示了lambda表达式的使用方法:

  • lambda表达式实际上就是匿名函数对象,可是一般不知道它究竟是什么类型。所以通过auto关键词把这个问题答案交给编译器来回答。
  • 捕获列表能够按值按引用两种方式来捕获信息。按引用方式进行捕获时须要考虑该变量生命周期的问题。由于lambda表达式作为一个对象是能够当做函数返回值跳出创建它的函数的范围。

    假设它通过引用方式捕获了一个函数内部的局部变量时,这个变量在跳出函数范围后将变得毫无意义,而且其占领的内存都可能不复存在了。

图45所看到的样例的捕获列表显示指定了要捕获的变量。假设变量比較多的话,要一个一个写上变量名会变得非常麻烦,所以lambda表达式还有更简单的方法来捕获全部变量,例如以下所看到的:

此处仅关注捕获列表中的内容

[=,&变量a,&变量b] = 号表示按值的方式捕获该lambda创建时所能看到的全部变量。

假设有些变量须要通过引用方式来捕获的话就把它们单独列出来(变量前带上&符号

[&,变量a,变量b] &号表示按引用方式捕获该lambda创建时所能看到的全部变量。假设有些变量须要通过按值方式来捕获的话就把它们单独列出来(变量前不用带上=号

1.7  STL介绍

STL是StandardTemplate Library的缩写。英文原意是标准模板库。由于STL把自己的类和函数等都定义在一个名为std(std即standard之意)的命名空间里。所以一般也称其为标准库。标准库的重要意义在于它提供了一套代码实现非常高效。内容涵盖很多基础功能的类和函数。比方字符串类,容器类,输入输出类,多线程并发类,经常使用算法函数等。尽管和Java比起来,C++标准库涵盖的功能并不算多,可是使用方法却非常灵活。学习起来有一定难度。

熟练掌握和使用C++标准库是一个合格C++程序猿的重要标志。

对于标准库。笔者感觉是越了解其内部的实现机制越能帮助程序猿更好得使用它。

所以,參考文献[2]差点儿是C++程序猿入门后的必读书了。

STL的内容非常多,本节仅从API使用的角度来介绍当中一些经常使用的类和函数。包括:

  • string类,和Java中的String相似。
  • 容器类。包括动态数组vector。链表list。map类、set类和相应的迭代器。
  • 算法和函数,比方搜索,遍历算法,STL中的函数对象,绑定等。
  • 智能指针类。

1.7.1  string类

STL string类和Java String类非常像。只是,STL的string类事实上仅仅是模板类basic_string的一个实例化产物。STL为该模板类一共定义了四种实例化类。如图46所看到的:

图46 string的家族

图46中:

  • 假设要使用当中不论什么一种类的话,须要包括头文件<string>
  • string相应的模板參数类型为char,也就是单字节字符。而假设要处理像UTF-8/UTF-16这样的多字节字符,程序猿可酌情选用其它的实例化类。

string类的完整API可參考http://www.cplusplus.com/reference/string/string/?

kw=string

其使用和Java String有些相似,所以上手难度并不大。图47中的代码展示了string类的使用:

图47 string类的使用

1.7.2  容器类

好在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

1.  vector类

vector是模板类,使用它之前须要包括<vector>头文件。图48展示了vector的一些常见使用方法:

图48 vector使用方法演示样例

图48中有三个知识点须要读者注意:

  • vector是模板类,本例用int作为模板參数实例化后得到一个名为vector<int>的类。这个类的名字写起来比較麻烦,所以我们通过using关键词为它定义了一个类型别名IntVectorIntVectorvector<int>的别名,凡是出现IntVector的地方事实上都是vector<int>。
  • 大部分STL容器类中都定义了相相应的迭代器,其类型名为Iterator

    C++中没有通用的Iterator类(Java有Iterator接口类),而是须要通过容器类::Iterator的方式定义该容器类相应的迭代器变量。迭代器用于訪问容器的元素。其作用和Java中的迭代器相似。

  • 图48中再次展示了auto的使用方法。

    auto关键词的出现使得程序猿不用再写冗长的类型名了。一切交由编译器来完毕。

关于vector的知识我们就介绍到此。

注意:

再次提醒读者,STL容器类的学习绝非知道几个API就能够的,其内部有相当多的知识点须要注意才干真正用好它们。强烈建议有进一步学习欲望的读者研读參考文献[2]。

2.  map类

map也叫关联数组。图49展示了map类的情况:

图49  map类

图49中:

  • map是模板类,使用它之前须要包括<map>头文件。map模板类包括四个模板參数,第一个模板參数Key代表键值对中键的类型。第二个模板參数T代表键值对中值的类型。第三个模板參数Compare,它用于比較Key大小的,由于map是一种按key进行排序的容器。第四个參数Allocator用于分配存储键值对的内存。STL中。键值对用pair类来描写叙述。
  • 使用map的时候离不开pair。

    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加入元素:

  • 通过索引Key的方式可加入或訪问元素。比方stringMap["4"]="four",假设stringMap["4"]所在的元素已经存在。则是对它又一次设置新的值。否则是加入一个新的键值对元素。该元素的键为"4"。值为"four"。
  • 通过insert加入一个元素。再次强调,map中元素的类型是pair,所以必须构造一个pair对象传递给insert。C++11前可利用辅助函数make_pair来构造一个pair对象,C++11之后能够利用{}花括号来隐式构造一个pair对象了。

map默认的Compare模板參数是std::less。它将按从小到大对key进行排序,怎样为map指定其它的比較方式呢?来看图51:

图51  map的使用方法之Compare

图51展示了map中和Compare模板參数有关的使用方法。当中:

  • decltype(表达式):用于推导表达式的数据类型。比方decltype(5)得到的是int,decltype(true)得到的是bool。decltypeauto都是C++11中的关键词,它们的真实类型在编译期间由编译器推导得到。
  • std::function是一个模板类。它能够将一个函数(或lambda表达式)封装成一个重载了函数操作符的类。这个类的函数操作符的信息(也就是函数返回值和參数的信息)和function的模板信息一样。比方图51中"function<bool (int,int)>"将得到一个类,该类重载的函数操作符为"booloperator() (int,int)"。

3.  allocator介绍

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中也定义了相似的分配器,以后我们会碰到它们。

1.7.3  算法和函数对象介绍

STL还为C++程序猿提供了诸如搜索、排序、拷贝、最大值、最小值等算法操作函数以及一些诸如less、great这样的函数对象。本节先介绍算法操作函数,然后介绍STL中的函数对象。

1.  算法

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中包括一些知识点须要读者了解:

  • 对于操作容器的算法函数而言。它并不会直接操作详细的容器类,而是借助Iterator来遍历一个范围(通常是前开后闭)内的元素。

    这样的方式将算法和容器进行了最大程度的解耦,从此,算法无需关心容器,而是仅仅通过迭代器来获取、操作元素。

  • 对刚開始学习的人而言。算法函数并不像它的名字一样看起来那么easy使用。

    以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_if将vector中值为-1的元素remove。

    可是这个元素会被remove到哪去?该元素所占的内存会不会被释放?STL中。remove_if函数仅仅是将符合remove条件的元素挪到容器的后面去。而将不符合条件的元素往前挪。所以,vector终于的元素布局为前面是无需移动的元素,后面是被remove的元素。可是请注意。vector的元素个数并不会发生改变。所以,remove_if将返回一个迭代器位置,这个迭代器的位置指向被移动的元素的起始位置。

    即vector中真正有效的元素存储在begin()newEnd之间,newEndend()之间是逻辑上被remove的元素。

  • 假设刚開始学习的人不知道remove_if并不会改变vector元素个数的话。就会出现图54中最后一个for循环的结果。vector的元素还是有6个,就好像没有被remove一样。

是不是有种要抓狂的感觉?这个问题怎么破解呢?当使用者remove_if调用完毕后。务必要通过erase来移除容器中逻辑上不再须要的元素。代码例如以下:

//newEnd和end()之间是逻辑上被remove的元素。我们须要把它从容器里真正移除!

aIntVector.erase(newEnd,aIntVector.end());

最后,关于<algorithm>的全部内容请读者參考:

http://en.cppreference.com/w/cpp/header/algorithm

2.  函数对象

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展示了:

  • mutiplies模板类:它是一个重载了函数操作符的模板类。用于计算两个输入參数的乘积。输入參数的类型就是模板參数的类型。
  • less模板类,和mutiplies相似。它用于比較两个输入參数的大小。

最后,关于<algorithm>的全部内容,请读者參考:

http://en.cppreference.com/w/cpp/header/functional

提示:

从容器类和算法以及函数对象来看。STL的全称标准模板库是非常名符事实上的,它充分利用了和发挥了模板的威力。

1.7.4  智能指针类

我们在本章1.3.3“->和*操作符重载”一节中曾介绍过智能指针类。C++11此次在STL中推出了两个比較经常使用的智能指针类:

  • shared_ptr:共享式指针管理类。内部有一个引用计数。每当有新的shared_ptr对象指向同一个被管理的内存资源时,其引用计数会递增。

    该内存资源直到引用计数变成0时才会被释放。

  • unique_ptr:独占式指针管理类。

    被保护的内存资源仅仅能赋给一个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的使用方法。

1.  shared_ptr介绍

图57为shared_ptr的使用方法演示样例,难度并不大:

图57 shared_ptr使用方法演示样例

图57中:

  • STL提供一个帮助函数make_shared来构造被保护的内存对象以及一个的shared_ptr对象。
  • 当item0赋值给item1时,引用计数(通过use_count函数返回)递增。
  • reset函数能够递减原被保护对象的引用计数,并又一次设置新的被保护对象。

关于shared_ptr很多其它的信息,请參考:http://en.cppreference.com/w/cpp/memory/shared_ptr

2.  unique_ptr介绍

ART中使用unique_ptr远比shared_ptr多,它的使用方法比shared_ptr更简单,如图58所看到的:

图58 unique_ptr使用方法演示样例

关于unique_ptr完整的API列表,请參考http://en.cppreference.com/w/cpp/memory/unique_ptr

1.7.5  探讨STL的学习

本章对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时最好还是了解下它们。

1.8  其它经常使用知识

本节介绍ART代码中其它一些常见知识。

1.8.1 initializer_list

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;

}

1.8.2  带作用域的enum

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};

1.8.3  constexpr

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函数不是常量表达式

上面代码中:

  • x定义为一个const整型变量,但由于expr函数会依据输入參数的不同而返回不同的值,所以x事实上仅仅是一个不能被改动的量,而不是严格意义上的常量常量的含义不仅仅是它的值不能被改变,而且它的值必须是固定的。
  • 对于这样的情况。我们能够使用constexpr来定义一个货真价实的常量。constexpr将告知编译器对expr函数进行推导。推断它究竟是不是一个常量表达式。

    非常显然。编译器推断expr不是常量表达式。由于它的返回值受输入參数的影响。所以上述y变量定义的那行代码将无法通过编译。

所以,constexpr关键词定义的变量一定是一个常量。

假设等号右边的表达式不是常量,那么编译器会报错。

提示:

常量表达式的推导工作是在编译期决定的。

1.8.4  static_assert

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。

1.9  參考文献

本章对C++语言(以C++11的名义)进行了浮光掠影般的介绍。

其内容不全面。细节不深入,描写叙述更谈不上精准。只是。本章的目的在于帮助Java程序猿、不熟悉C++11可是接触过C++98/03的程序猿对C++11有一个直观的认识和了解,这样我们将来分析ART代码时才不会认为陌生。对于那些有志于更进一步学习C++的读者们,以下列出的五本參考书则是不可缺少的。

[1]  C++ Primer中文版第5版

作者是Stanley B.Lippman等人。译者为王刚,杨巨峰等。由电子工业出版社出版。假设对C++全然不熟悉,建议从这本书入门。

[2]  C++标准库第二版

作者是Nicolai M.Josuttis。此书中文版译者是台湾著名的IT作家侯捷。C++标准库即是TL(Standard Template Library,标准模板库)。相比Java这样的语言。C++事实上也提供了诸如容器,字符串。多线程操作(C++11才正式提供)等这样的标准库。

[3]  The C++Programming Language 4th Edition

作者是C++之父Bjarne Stroustrup,眼下仅仅有英文版。这本书写得非常细。由于是英文版,所以读起来也相对费事。另外。书里的演示样例代码有些小错误。

[4]  C++ ConcurrencyIn Action

作者Anthony Williams。C++11标准库添加了对多线程编程的支持,假设打算用C++11标准库里的线程库,请读者务必阅读此书。这本书眼下仅仅有英文版。说实话,笔者看完这本书前5章后就不打算继续看下去了。由于C++11标准库对多线程操作进行了高度抽象的封装。这导致用户在使用它的时候还要额外去记住C++11引入的特性,非常麻烦。所以。我们在ART源代码中发现谷歌并未使用C++11多线程标准库。而是直接基于操作系统提供的多线程API进行了简单的,面向对象的类封装。

[5]  深入理解C++11:C++11新特性解析与应用

作者是Mical Wang和IBM XL编译器中国开发团队,机械工业出版社出版。

[6]  深入应用C++11代码优化与project级应用

作者祁宇,机械工业出版社出版

[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就是该类的实例,其数据类型随之确定。

猜你喜欢

转载自www.cnblogs.com/fnlingnzb-learner/p/9499430.html