《Beginning C++20 From Novice to Professional》第十二章 Defining Your Own Data Types (上篇)

终于到了介绍面向对象和类的环节

说实话东西有点多,可以分两章来写,this指针之前的会在这篇文章涉及

Classes and Object-Oriented Programming

之前我们写的代码都属于过程式,将解决问题分解为一个个步骤然后分步完成;但是这种方法不能模拟现实世界,可能做做计算比较合适,在大型工程中需要模拟社会与人类认知事物的方式,此时人们发明了类(C中结构体进化而来),用来将基本类型与一些操作组合在一起构建新类型

然后我们以这些原则为指导写面向对象的程序,此时的基础不再是基础类型与运算,而是对象的方法与对象交互

Encapsulation 封装

基本上,我们定义事物的方式都是提取它的特性,其他事物没有的性质来对事物下定义,比如一个正方体知道边长是特征,长方体长宽高是特征,球体半径是特性,银行账户的存款、利率等是特性,银行的地点、名称、负责人等等是特征

当然这些事物或者对象不只有这些静态标签,还有自己的动作,我们叫方法;球体可以滚动,人可以吃饭睡觉等等,是一些基于自己的属性进行的行为

这样将数据和方法组合到一个实体上的行为就是封装,有点像饭点打包,将我们需要的东西都用袋子装起来;下面是一个封装示意图,以银行贷款账户为例

注意我们这里定义对象属性的原则是符合我们需要,就像之前的正方体,如果我们只需要体积表面积,那么我们只需要给它一个边长属性;如果我们需要颜色、重量、材质,那么有必要加上其他的属性

Data Hiding

还是说回之前的银行账户,我们不希望这种重要数据被随便更改,那么引入的保护数据与方法的技术叫做数据隐藏或者信息隐藏

这个技术在C++里通过访问权限来实现,后面会讲到

Inheritance 继承

继承又是一个符合人类认知事物的概念,我们对生物分门、科、属,对人类分性别、阶级、工种,对建筑分风格、年代,本质上都是继承于基础概念后的细分过程

代码也是一样,模拟现实世界的时候也可以有继承下的细分类,比如我们定义棱台为一个基础类,那么下面可以派生出长方体为一个特例,因为长方体有三对相同的面,长方体下又可以派生出正方体,它的六个面都相同,但所有派生类都基于一个棱台,拥有八条边六个面的表面平整的立方体

Polymorphism 多态

这里指的是动态多态,因为多态也分很多种,像函数模板、类模板也属于多态,函数重载、操作符重载也属于多态,不过后面的都是编译期的操作

动态多态就是程序运行时能够基于不同的类型给出不同的结果,这里的类型指的是对象类型,具体来说是继承链上的对象类型

C++中动态多态通过指针或引用调用成员函数来实现,背后的机制是虚函数

Defining a Class 

类属于自定义类型,定义或声明的时候使用class关键字:

再加上封装时讲到的数据保护技术,举个例子:

我们把不想被外部访问的数据、方法写在private标签后,希望外部可以调用的写在public后

一般public写在前,因为我们通常关注它的功能而不是具体实现;数据一般写在写在方法后面构造函数和析构函数一般写在最前面

当然我们也可以用结构体,C中组织数据的方式,结构体可能对于我们这种小程序更简单,因为struct成员默认访问权限是public

class 和 struct 的唯一区别,是访问权限(包括继承的访问权限)默认是 private 还是 public
其它方面 class 和 struct 完全一样

我们应该多使用结构体而不是tuple或者pair这种工具类型,因为这样语义更明确

Creating Objects of a Class

把类名字当int、double这种普通类型使用就可以了

因为有了访问权限的限制,数据成员不可以被外部访问,所以我们只能调用public的部分

Constructors 构造函数

构造函数有以下特点:

  1. 名字与类名相同
  2. 没有返回值类型,没有返回值

创建对象只能用构造函数,只要有对象被创建,对应的构造函数就被调用

Default Constructors 默认构造函数

注意,这里的默认构造函数是我们没有写构造函数时,编译器生成的一个默认版本,以保证类的正常构造,这个构造函数什么也不做 

所以实际上,Box这个类有一个编译器生成的构造函数,大概是下面这个样子:

默认构造函数指的是构造对象时不需要实参的构造函数,通常也意味着对象会被默认初始化

书上管这个叫default default constructor,因为这是默认情况下生成的默认构造函数,默认情况下指我们不写构造函数,默认构造指进行默认初始化的构造函数,唯一的作用就是保证对象能够构造

什么也不做的默认构造函数将产生什么后果呢?对于是基础类型的数据成员,什么也不做意味着数据里都是垃圾值,包括算数类型和指针

不过我们的Box类给了初始值,所以构造后数据成员的值都是有意义的(至于这个初始化顺序后面会讲到)

Defining a Class Constructor 构造函数的定义

虽然一般工程函数不会输出这种提示信息,但是写小程序用来学习或者调试的时候,输出一个定位信息或者局部变量是很好的~

之前已经说过,默认构造函数只有在我们没写的时候编译器才会生成,此处写了那就用写的版本,编译器不会再生成;话说回来,这个在《Primer》里叫做合成构造函数,我觉得这个名字更合理;所以下面28行处不会通过编译,因为已经不支持默认初始化了

有的写法会使用this->length=length的方式来初始化,但是书里还是采用不同的名字来进行编写,避免名字冲突造成歧义

Using the default Keyword 

如果我们还是想要默认构造这样一个功能怎么办呢?

一个方法是我们自己写,什么参数都没有,什么效果也不做(这一点有待商榷)

还有一个方法是使用default关键字

there are also a few subtle technical reasons that make the compiler-generated version the better choice.

默认无参构造函数一定是空函数吗? - 知乎

这里书上提到了一点,编译器生成的版本和我们写的空构造函数并不完全相同,这一点需要深入学习,但是对于简单类型不需要考虑这个问题(暂时认为语法糖即可)

Defining Functions Outside the Class

这一点和之前模块那里说的没有区别,是把声明和实现分开的一种写法,我们可以把实现移到cpp里而不是放在头文件里

Default Arguments for Constructor Parameters

和普通函数类似,构造函数也可以有默认参数

更类似的是,全带默认参数的构造函数和无参构造函数其实没有区别

这样会编译失败的,我们无法确定没有参数的时候是调用哪个版本;这种情况通常是把第一个删去,因为没有默认值通常没有什么实际意义

Using a Member Initializer List

我们不在函数体里写赋值语句,而是在参数列表之后加冒号,然后用大括号给数据成员以初始值,这种写法叫成员初始值列表

这种写法的好处是(我们需要首先了解一下构造对象的数据成员的过程):函数体内写赋值是在初始化数据成员之后的重新赋值,而初始值列表采用初始值直接进行初始化,中间少了一步所以会快很多,尤其是成员是对象的时候

这种写法的坑在于:数据成员初始化的顺序和列表里的顺序无关,而是和他们在类声明中出现的顺序有关;如果有的成员依赖其他成员进行初始化,我们需要特别注意这个顺序

Using the explicit Keyword

A problem with class constructors with a single parameter is that the compiler can use such a constructor as an implicit conversion from the type of the parameter to the class type.

我们尤其要注意只有一个参数的构造函数,如果不加以限制,很多时候这个构造函数会进行没必要的隐式类型转换

举个例子:

这个构造函数只有一个double参数,我们看26行,这个方法的参数居然是一个double类型!

虽然很符合直觉,好像这个地方参数就应该提供数值和对象两种,但是我们应该比的是体积,可这个地方其实用50.0这个double构造了一个临时对象,然后拿box1的体积去与50^3比较了,所以其实这个隐式转换是不符合我们预期的,这样的构造函数ref叫做converting constructor

C++11之后除了单参数构造函数不加explicit的构造函数都是转换构造函数

为了避免这个问题,我们需要在这种构造函数前面加上一个explicit关键字:

再举个多参构造的例子:

不加explicit都是可以转换的,我们可以看看上面转换构造的定义

加了explicit之后,这些都不能过编译了,必须显式调用构造函数

Delegating Constructors 委托构造函数

这是一种代码复用的实现,构造函数完全可以有多个接口,但是实现不需要那么多,可以嵌套构造来简化实现

我们可以实现一个通用的三参版本,然后第二个构造函数复用第一个实现来简化:

不过之前看到的都是小括号的版本,由于C++20之后更推荐大括号初始化,所以初始值列表这里要习惯一下

可以看到30行这个对象的构造使用了两个构造函数体

但是要注意的是,委托别人的构造函数本身也是构造函数,这种委托构造函数也有一定的限制

If the name of the class itself appears as class-or-identifier in the member initializer list, then the list must consist of that one member initializer only

首先,会进行函数的嵌套调用;其次,它的初始值列表里不可以包含其他的内容,要委托就只能委托,不能进行其他成员的初始化动作

The Copy Constructor 复制构造函数

我们用之前的box2对象对box3初始化,这种复制的行为也很自然,但是我们并没有写这种构造函数,box3是怎么进行构造的呢?

实际上复制构造函数不写的话编译器也会生成一个合成的默认版本,通过复制现有对象的数据进行复制初始化

Copy constructors - cppreference.com

Implementing the Copy Constructor

复制构造函数只能接受一个同类型的参数,但是这样会产生上面这样的问题,我们的本意是用一个已有对象来初始化一个新对象,如果采用值传递的参数,类又复制了一次,可是我们根本没有实现复制行为!这样导致了复制构造的递归!

所以复制构造函数的参数必须是引用,而且一般是const-ref,首先因为我们不修改原有版本,只是创造一个副本;其次const引用可以接收非const,如果我们写了非const参数,那么const对象不能接收也就无法复制了

当然我们也可以把这个构造函数委托给之前那个通用版本

~但是完全没有必要,因为不牵扯指针浅复制的时候,大部分时间我们都可以用默认版本而不需自己编写

Deleting the Copy Constructor

有的时候我们也不想对象被复制,此时可以给一个删除关键词

此时对象复制行为将被禁止,这样的代码无法通过编译

此外,成员里有对象的复制构造函数被删除或者私有时,这个类的复制构造函数都会被删除,即使我们显式要求编译器生成default版本

Whenever you delete the copy constructor, you probably also want to delete the so-called copy assignment operator. We will tell you more about this in the next chapter.

这里就是后话了,复制行为除了构造还有赋值,这两个一般要删一起删

Defining Classes in Modules

export关键字要放在类的声明前;并且export不影响类原有的访问控制权限

当然我们还是可以把实现挪到cpp里和声明分开

Accessing Private Class Members 访问私有成员

我们不想把数据完全封闭起来,应该授权给可以修改它的对象进行修改,但是不应该在外部直接读取,所以经常会见到这种形式,一般叫getter/setter,用来修改数据;如果是bool类型那一般起名都是is什么,比如isValid()这样可读性比较好

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

+xiaowenhao+

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值