Knowledge center

带你读《C++语言导学》之二:用户自定义类型-阿里云开发者社区(en)

2019-11-07 00:00:00 mimukeji

点击查看第一章
点击查看第三章

第2章

A Tour of C++, Second Edition

用户自定义类型

不必惊慌失措!—道格拉斯·亚当斯

2.1 引言

用基本类型(参见1.4节)、const修饰符(参见1.6节)和声明运算符(参见1.7节)构造出来的类型,称为内置类型(built-in type)。C++的内置类型及其操作非常丰富,不过有意设计得更偏底层。这些内置类型能直接、高效地反映传统计算机硬件的能力,但是没有为程序员提供便于编写高级应用程序的高层设施。取而代之,C++在内置类型和操作的基础上增加了一套精致的抽象机制(abstraction mechanism),程序员可用它来构造所需的高层设施。
C++抽象机制的目的主要是令程序员能够设计并实现他们自己的数据类型,这些类型具有恰如其分的表示和操作,程序员可以简单优雅地使用它们。利用C++的抽象机制从其他类型构造出来的类型被称为用户自定义类型(user-defined type),即类(class)和枚举(enumeration)。用户自定义类型可以基于内置类型构造,也可基于其他用户自定义类型构造。本书的大部分内容都在着重介绍用户自定义类型的设计、实现和使用。用户自定义类型通常优于内置类型,因为其更易用、更不易出错,而且通常与直接使用内置类型实现相同功能一样高效,甚至更快。
本章的剩余部分将呈现类型定义和使用相关的最简单同时也是最基础的语言设施。第4~7章对抽象机制及其支持的编程风格进行了更加详细的介绍。第8~15章给出标准库的概述,因为标准库主要是由用户自定义类型组成的,所以这些章节也提供了很好的示例,展示了用第1~7章介绍的语言设施和编程技术能做什么。

2.2 结构

构造新类型的第一步通常是把所需的元素组织成一种数据结构,即一个struct:
image.png
这是Vector的第一个版本,它包含一个int和一个double*。
Vector类型的变量可像这样定义:
image.png
但是,就v本身而言,它的用处似乎不大,因为v的elem指针并没有指向任何东西。为了让它变得有用,我们必须给出一些元素,令v指向它们。例如,我们可以构造一个如下所示的Vector:
image.png
也就是说,v的elem成员被赋予了一个由new运算符生成的指针,而v的sz成员则得到了元素的数目。Vector&中的&指出,我们是通过非const引用(参见1.7节)方式传递v的,这样vector_init()就能修改传给它的向量了。
new运算符从一块名为自由存储(free store)(又称为动态内存(dynamic memory)或堆(heap))的区域中分配内存。在自由存储中分配的对象独立于它创建时所处的作用域,会一直“存活”到使用delete运算符(参见4.2.2节)销毁它为止。
Vector的一个简单应用如下所示:
image.png
显然,我们的Vector在优雅程度和灵活性上与标准库vector还有很大差距,尤其是Vector的使用者必须知道有关其表示方式的所有细节。本章余下的部分以及接下来的两章会逐步改进Vector,作为呈现语言特性和技术的一个示例。作为对比,第11章会介绍标准库vector,其中包含着很多良好的改进。
本书使用vector和其他标准库组件作为示例,以

  • 展现语言特性和设计技术
  • 帮助读者学会使用这些标准库组件。

不要试图重写vector和string等标准库组件,直接使用它们更为明智。
我们可以通过名字(或引用)访问struct的成员,此时使用.(点运算符);也可通过指针访问struct的成员,此时使用->。例如:
image.png
image.png

2.3 类

将数据说明与其操作分离开来有其优势,例如我们可以以任意方式使用数据。但对于用户自定义类型来说,为了具备“真正的类型”所需的所有性质,在其表示形式和操作之间建立紧密的联系是很有必要的。特别是,我们通常希望保持数据表示对用户不可见,从而实现易用性、保证数据使用的一致性以及允许设计者未来改进数据表示。为此,我们必须将类型的接口(所有人均可使用)与其实现(可访问对外部不可见的数据)分离开来。在C++中,实现上述目的的语言机制称为类(class)。类含有一系列成员(member),它可以是数据、函数或者类型。类的public成员定义了接口,private成员则只能通过接口访问。例如:
image.png
在此基础上,我们可以定义新类型Vector的一个变量:
image.png
下图解释了这个Vector变量的构成:
image.png
本质上,Vector对象是一个“句柄”,它包含指向元素的指针(elem)以及元素数目(sz)。在不同Vector对象中元素数目可能不同(本例是6),即使同一个Vector对象在不同时刻也可能含不同数目的元素(参见4.2.3节),但Vector对象本身的大小永远保持不变。这是C++语言处理可变数量信息的一项基本技术:一个固定大小的句柄指向位于“别处”(如通过new分配的自由空间,参见4.2.2节)的一组可变数量的数据。第4章的主题就是学习如何设计并使用这样的对象。
在这里,我们只能通过Vector的接口访问其数据表示(成员elem和sz),而接口是由其public成员提供的:Vector(), operator[]()和size()。这样,2.2节的read_and_sum()示例可简化为:
image.png
image.png
与所属类同名的成员“函数”称为构造函数(constructor),即,它是用来构造类的对象的。因此构造函数Vector()替换了2.2节的vector_init()。与普通函数不同,编译器会保证在初始化类对象时使用构造函数,因此,定义构造函数可以消除类变量未初始化问题。
Vector(int)规定了Vector对象的构造方式。特别是,它声明需要一个整数来构造对象。这个整数用于指定元素数目。构造函数使用成员初始化列表来初始化Vector的成员:
image.png
这条语句的含义是:首先从自由空间获取s个double类型的元素,然后用指向这些元素的指针初始化elem;然后使用s初始化sz。
访问元素的功能是由下标函数opeartor[]提供的,它返回所需元素的引用(double&,既允许读也允许写)。
size()函数的作用是向使用者提供元素数目。
显然,我们完全没有涉及错误处理,但将在3.5节提及。类似地,我们也没有提供一种机制来“归还”通过new获取的double数组,4.2.2节将介绍如何使用析构函数来优雅地完成这一任务。
struct和class没有本质区别,struct就是一种成员默认为public的class。例如,你也可以为struct定义构造函数和其他成员函数。

2.4 联合

union是一种特殊的struct,它的所有成员被分配在同一块内存区域中,因此,联合实际占用的空间就是它最大的成员所占的空间。自然,在某个时刻,一个union中只能保存一个成员的值。例如,一个符号表表项结构保存一个名字和一个值,值可以是一个Node*或一个int:
image.png
因为p和i永远不会同时使用,所以浪费了内存空间。通过将两者定义为一个union的成员,可以很容易地解决该问题,如下所示:
image.png
C++不会记录一个union保存了哪种值,因此程序员必须自己做这个工作:
image.png
维护类型域(type field,在本例中是t)与union中所存类型的对应关系很容易出错。为了避免错误,我们可以强制这种对应关系—将联合和类型域封装在一个类中、只允许通过能正确使用联合的成员函数来访问它们。在应用层面上,依赖这种标记联合(tagged union)的抽象很常见也很有用。我们应尽量少地使用“裸”union。
在大多数情况下,我们可以使用标准库类型variant来避免直接使用union。一个variant保存一组可选类型中一个类型的值(参见13.5.1节)。例如,一个variant,int>可以保存一个Node或一个int。
使用variant,Entry的例子可改写为:
image.png
对于很多应用,使用variant都比使用union更简单、更安全。

2.5 枚举

除了类之外,C++还提供了一种形式简单的用户自定义类型,可以用来枚举一系列值:
image.png
注意,枚举值(如red)位于其enum class的作用域之内,因此我们可以在不同的enum class中重复使用这些枚举值而不致引起混淆。例如,Color::red是指Color的red,它与Traffic_light::red显然不同。
枚举类型常用于描述规模较小的整数值集合。通过使用有指代意义的(且易于记忆的)枚举值名字,可以提高代码的可读性,降低出错的风险。
enum后面的class关键字指明了枚举是强类型的,且它的枚举值位于指定的作用域中。不同的enum class是不同的类型,这有助于防止对常量的意外误用。例如,我们不能混用Traffic_light和Color的值:
image.png
同样,我们也不能隐式地混用Color和整数值:
image.png
捕捉试图向枚举类型的转换是避免错误的一种好的防御措施,但我们常常希望用枚举类型的基础类型(默认是int)的值对其初始化,这就要允许从基础类型隐式转换为枚举类型:
image.png
默认情况下,enum class只定义了赋值、初始化和比较(如==和<,参见1.4节)操作。然而,既然枚举类型是一种用户自定义类型,那么就可以为它定义别的运算符:
image.png
如果你不想显式地限定枚举值名字,并且希望枚举值可以是int(无须显式转换),你可以去掉enum class中的class而得到一个“普通”enum。“普通”enum中的枚举值的作用域与其enum的作用域一致,并且会隐式地转换成整数值。例如:
image.png
在这里,col的值是1。默认情况下,枚举值对应的整数从0开始,依次加1。“普通”enum很早就出现在C++和C中了,所以即使它的效果并不是那么好,在当前的代码中仍很常见。

2.6 建议

[ 1 ] 当内置类型过于底层时,优先使用定义良好的用户自定义类型;2.1节。
[ 2 ] 将有关联的数据组织为结构(struct或class);2.2节;[CG: C.1]。
[ 3 ] 用class表达接口与实现的区别;2.3节;[CG: C.3]。
[ 4 ] 一个struct就是一个成员默认为public的class;2.3节。
[ 5 ] 定义构造函数以保证和简化类的初始化;2.3节;[CG: C.2]。
[ 6 ] 避免使用“裸”union;将其与类型域封装在一个类中;2.4节;[CG: C.181]。
[ 7 ] 用枚举类型表达一组命名的常量;2.5节;[CG: Enum.2]。
[ 8 ] 与 “普通” enum相比,优先使用class enum,以避免很多麻烦;2.5节;[CG: Enum.3]。
[ 9 ] 为枚举定义操作来简化使用、保证安全;2.5节;[CG: Enum.4]。

(en)

阿里云优惠新机+优惠券

本文转载自网络,如有侵权,请联系我们删除。

Home

About

product

success

news

form

bbs

contact

工单(en)

阿里云报价咨询(en)