加载中...
Hello Object 生死版.上
第1节:初学C++,应该学什么?
第2节:《白话C++》练的什么“功”?
第3节:《白话C++》练的什么“武”?
第4节:打开浏览器,线上玩转C++
第5节:逐字逐句,深入理解C++最小例程
第6节:做一个“会”犯错误的程序员
第7节:Hello World 中文版
第8节:Hello World 函数版
第9节:Hello World 交互版
第10节:Hello World 分支版
第11节:Hello World 循环版
第12节:Hello Object 生死版.上
第13节:Hello Object 生死版. 下
第14节:Hello Object 成员版
第15节:Hello Object 派生版
第16节:Hello Object 多态版
课文封面

你好对象!

  1. 认识C++中基础中的基础类型;
  2. 创建用户自定义的复合类型;
  3. 创建新类型的对象;
  4. 定制新类型对象的生死过程

零、视频

一、面向对象启蒙

之前我们一直在问候世界,从这节课开始,我们的问候对象就是“对象(Object)”了。
今天是这个系列的第一节:《Hello Object 生死版.上》。

今天这节课,我们要学习如何创建物种以及如何创造对象,展开讲,包括:

  1. 在C++代码中创建一个新物种;
  2. 为这个新物种创造一个对象;
  3. 定制新创建的物种对象出生与对象死亡的过程。

这里提到了两个关键概念:物种和对象,大家能理解二者的区别吗?

“物种”就是一种生物的类型,而“对象”,是属于这种生物类型的,一只活生生的实体。比如,作为物种,恐龙(物种)是存在的,但是,今天的这个世界里,已经不存在,具体的某一只恐龙(对象)了。

之所以要把“对象”设定为后面许多课的重要学习对象,是因为有一种重要的编程思想叫作“面向对象编程”;英文是 Object Oriented Programming,简称OOP——面向对象编程。

直译的话就是“以对象为导向的方法来编程”,也可以理解为:围绕对象来设计、组织你的代码结构。

新知识通常需要以旧知识作为基础。之前我们已经写过至少6个版本的“问候”程序。比如:函数版、交互版、分支版、循环版等等。当时我们所使用的,是什么编程思想呢?有没有一种可能,我们一直都在写“没有思想”的代码呢?

当然不是。之前我们一直在使用“面向过程” (Procedure Oriented Programming)的思想在编写代码。

面向过程是一种特别自然而然的思想,因为在生活中我们就是这样处理事情的:

面向过程思想:把做一件大事情拆解为做几件比较小的过程。如有必要,再把一个小的过程拆成更小更小的过程。

面向过程就是一种典型的“大事化小,小事化了”的思路体现。

把“过程”对应到“函数”,大家是不是马上就能够认同:之前我们写的问候程序采用的是“面向过程”的方法。在 Hello World 例程的第一个版本,也就是第五节,《逐字逐句深入理解C++最小例程》,我们就讲过:

“把程序理解成是在做一件大事,那么,这件大事对应的就是main主函数”

而后的课程,在需求的不断增加与实现版本的不断演化的过程中,我们慢慢的把 “上班路上问候同事”这件事情(对应 main() 主函数)拆成两个小过程(函数):

  • Input() 函数:用来模拟实现遇到同事;
  • Hello() 函数:用来模拟实现问候同事。

“大事化小,小事化了”,“面向过程”的编程思路确实是非常地自然而然,但是,如果一件复杂的事情大到一定程度的时候,就会出现事情仿佛千头万绪,我们甚至不知道如何开始的情况。

举个例子:假设2030年,国家又要承办一场奥运会,而你突然被全国人民推到该届奥运总负责人的岗位,此时,你觉得自己应该如何开始?

正确的方法应该是:找人,找人才,找各项事情的总负责人。开幕式、场地、安全保障,经济收入、来宾接待,传播以及赛事自身,等一大堆事,你不可能样样擅长——你甚至一样都不擅长,因此你必须为每类事情,都找一个总负责人。

为不同类型的事情,找一个具备处理此类事情的方法的负责人。这里面有四个关键字:类型、事情、方法、负责人。其中“事情”还是对应到“函数”,而类型、方法、负责人对应C++代码中的什么呢?
答案如下:

现实世界 代码世界 备注
事情 普通函数 已学
类型 数据类型,特别是用户自定义复合类型 -
方法 和类型紧绑定的函数:成员函数 也经常就被称为:方法

小结一下: 当问题慢慢变复杂,作为解决者,我们所考虑的,将慢慢地从具体某一件事情要如何解决,转成为某一类事件,找一个具备处理方法人才;这就是面向过程到面向对象的转换的最初原因。

二、类型即约束

面向对象的编程无法一蹴而就,今天我们只需建立对“对象”的一种感性认识即可。那么,什么叫对象? 老办法,让我们还是从问题、从需求开始说起。之前我们写的问候程序,一直在使用一个人的名字叫什么来判断这个人是不是我们心目中的女神,如:

if (XXX == "志玲") { cout << "亲爱的女神!" << endl; }

这种判断方法存在重大的逻辑漏洞。天底下叫“志玲”的人多去了,但并不是叫“志玲”的人都是我闪心目中的女神。

一个数据的名字,对这个数据会有什么内容是无法保证的,即:数据的名字,对数据的内容,以及数据的外在表现,都不会有实质的约束力。

那么,什么可以约束一个数据的内容呢?
答:数据的类型!

比如,作为数据,作为对象,我们都是人,我们的类型就是“人类”,而“人类”这个类型,对人的约束,就是“要做人事”。如果有人不做人事,我们就会鄙视并痛哭他:“你真不是个人!”,这是一种伤人很深的骂法,因为,它从类型上否定了一个,相当于从根源上否定了一个人。

类似的,如果有人女生偶尔骂你 “你真是头猪”,相信你并不会太在意,因为对方通常只是在嫌你在某些事情上笨了一些。如果这个女人骂的是 “你真不是个人……”你觉得怎样?

有没有比 “你真不是个人” 更狠的呢?当然有,一会儿我们就会给出答案。

三、C++常见内置基础类型

类型是这么的重要,所以编程语言都会内置一些常用的基础类型,我们先来快速学习C++的五种基础中的基础类型,重点观察不同类型对各自数据的约束是什么?

3.1 整数类型 / int

int 是 integer的缩写。

既然是整数,肯定得能参于加(+)减(-)乘(*)除(/)等数学计算的;其中有细则,比如 0 不允许当除数。

就像网上常有的口号 “不怎样怎样,你就不是什么什么”一样,如果有个数,它无法参加加法计算,我们就可以“指责”它:“你丫的肯定不是个整数!”。

整数不带有小数的信息,因此,如果两个整数做除法,比发 7 / 2 ,将得到另一个整数:3,而不是 3.5。

int a = 7 / 2; // 得3,而非3.5

3.2 字符类型 / char

char 是 character 的缩写。

这里有一句话:“远上寒山石径斜”,请问它包含几个字?答7个。
字符就是一句话,一段文本中的一个字,或者一个标点符号,也包括空格、换行、缩进符等等。
可惜,在C/C++被发明的时候,计算机并未足够全球普及,因此,汉字并未被C/C++语言视为一个字符。因此,上面例子,只能改为:

这里有一句话:“How are you?”,请问它包含几个字符?答:12个。

计算机中,一切数据本质都是数,char 也能被无损地转成 int 类型,依情况被转换至 -128 ~ 127或者 0 ~ 255 之间的某个整数。

C/C++字符类型对数据,也就是字符的约束还有:一个字符数据只能存储在一个字节(byte)的内存里。字节是计算机程序可以单独处理的最小内存单位。有多小呢?最常见的情况,正如上一段所说的结果:小到只能存放 256 种数据——而这也正是C/C++中的字符无法存储汉字的原因,中文何止 256 个汉字!

在C/C++代码中,字符的字面表达,需要使用单引号包含,如:

char a = 'A'; char b = 'b'; char c = ',';

3.3 布尔类型 / bool

bool 是 boolean 的缩写。

现实中或许并没有非黑即白的事,但是有些事情,真就是真,假就是假。表达这种非真即假,非假即真的数据,就需要用 布尔(boolean)类型。

比如,女朋友问你:“爱我吗?”,你的答案也许可以使用 int 类型,回答她:“我爱你100”——但是,我们的专业建议是,请使用 bool 类型。

C++中,用关键字 true 表示真,false 表示假。二者都可以无损转换为 int 类型。其中 false 将被转换为 0,而 true 将被转换为 1,因此,尽管没什么意义,但是计算:

int v = false + true;

这样代码是合法的,v 将得到 1 。

反过来,如果将 int 转换成 bool 类型数据,就是有损的了。其中 0 被转为 false,所有非 0 都被转为 true (很多信息,被弱化成一个信息,这就叫有损转换)。

bool b1 = true; bool b2 = 999; int v = b1 + b2; // v 是多少?

上例中,v 将得到 2。

3.4 浮点类型 / float / double

带小数信息的数据,比如 3.14、2.5、99.999、100.0,在计算机中通常使用一种叫“浮点”,也就是 “float point” 的格式存储、表达。注意,当你刻意地写 “100.0”时,它就不是一个整数,而是一个正好小数位是0的浮点数。

和生活中一样,在计算机,表达、存储、计算带小数的数,代价都远大于整数(所以小学生都是先学整数的四则运算,后面才学的小数、分数)。为了避免浪费,C++将浮点数,又按能表达的精度范围的大小,分成两种:单精度浮点数和双精度浮点数,即 float 和 double。具体的精度可以将来再了解,现在只需要建立感性认识,请看例子:

float f = 3.14159265358; double d = 3.14159265358;

例中,如果你再将 f 输出,会发现仅前面的 3.141592 是正确的,其后的小数位,可能都是错的。而 d 就没有这个问题。

3.5 标准库基础类型

除了语言内置的简单类型以外,C++标准库还提供了不少常用的基础类型,注意,它们都不是语言内置的,而且也并不算“简单”。它们被称为“标准库基础类型”,有别于前述的“内置基础类型”。

我们已经简单学习及多次使用过的标准库基础类型,这它俩:

  • std::string(标准库字符串类型)
  • std::string_view(字符串视图类型、即喜欢偷看的 “字符串观察者”)

四、用户自定义复合类型

世间的事物这么丰富、复杂,如果我们只有语言的内置类型,什么 int、char、bool、float、double……哪怕再加上标准库提供的基础类型,也是无法表达的。比如,人,应该用什么类型表达?

之前,我们就一直使用 std::string (字符串)来表达人的姓名,这自然很好,因为姓名本来就是字符串。不好的地方,或者存在严重逻辑错误的是:我们用姓名代表一个人,并且,还用姓名叫什么,来判断一个人是不是我们心目中的女神……错大发了!

为了解决这个问题,计算机编程语言支持我们通过组织简单类型,来形成复杂类型,全称是“用户自定义复合类型”。这里的“用户”就是指程序员,也就是我们。而“复合”,则是在表明:新类型是由多个类型组合起来的。

组合过程是可递进的。比如,假设将 int、float、bool 三者组成一个新类型,称为 T1, 那么,可以将 T1 和 char、std::string 再组成又一个新类型。

int、float、bool 的组合,大家能想象一些实际用途吗?

我想了两个:

  • int : 表达年龄;
  • float:表达每月收入,且因收入不高,又仅精确到分,故float精度够用,无需double;
  • bool :是否低保户;

显然,贫穷限制了南老师的想象力,再来一个——

  • int : 表示本学期的第几次英语单元考;
  • float: 表示得分(小数部分只需有 0.0 / 0.5 );
  • bool: 是否低于本次考试全班平均分;

显然,这次限制南老师想象力的,应该是长期低学分的学渣身份。

五、实战定义新复合类型

前面举的例子,于本课的学习目标来说,都过于具体、过于细节了,无法体现本课学习的高度。我们的高度是:当代码世界中的上帝,随心所欲地创建类型。

尽管在史上真的存在过恐猪(daeodon)和恐蛇(dinilysia)物种,但是,我们将无视这一事实,自行创建我们心目中的“恐猪”和“恐蛇”类型,对应的英文名字也不一样,分别是:DinosaurPig 和 DinosaurSnake。

5.1 定义新结构类型

以 DinosaurPig 类型为例,定义一个新的复合类型的最简语法是:

struct DinosaurPig { };
  • 关键字 “struct” 来自 “structure 结构”,很好理解:通过组合一些成员,从而到一种新的结构类型嘛,小时候玩过积木的同学,肯定理解。

  • “struct” 之后(有至少一个空格)就是新类型的名字,此例为 DinosaurPig;

  • 名字之后需要一对花括号,这和我们之前写函数体很像,关键区别见下一点;

  • 在定义新类型最后,即 ‘}’ 之后,需要有一个分号;

请同学们自己写出恐蛇的定义。

就这样吗?这就是 “恐猪” 的结构??我们只定义了新的复合类型的名字,却没有为这种复合类型定义任何内部组成的类型及数据。比如,恐猪有没有长角?有没有獠牙?有没有翅膀?我们统统不管了,别问为什么,问就是想象力很贫乏。

结构名字之后的那对花括号内部,确实就是用来定义新类型应该有哪些类型的组成部分的——但如前所述,这些知识点对本课而言,过于具体、过于细节了,高度不够。

尽管有名无实,但我们确实定义了一种新类型,只是没有在这个类型里提出它对本类数据的约束。不过,有一种类型,它就真的无需对数据进行约束。

请思考:如果我们要为世间的万物,定一个类型,那么:

  • 第一、这个类型应该叫什么名字?
  • 第二、这个类型又该由哪些类型组合而成?

更具体一点,请你叫来一个小学生,然后问他:风、国画、牛、石油、棒棒糖的共同分类是什么?

小学生肯定要冲你翻白眼。

冲你翻白眼的小学生肯定没有学过面向对象编程,并且多半没有读过《般若波罗蜜多心经》中的“色即空、空即色”,或者《道德经》中的 “无名天地之始,有名万物之母”。

用中文表达的话,世间万事万物的共同类型,就应该叫 “事物类型”,或者 “东西类型”。因为一切都是东西。比如正在阅读本文的你,请问,为师我可以说你“不是个东西”吗?

男人女人都是人类,而狗、蛇、虫子、人都是动物类型,而 花、草、动物都是生物类型;而石油、花、人、狗、风、国画、牛、棒棒糖,它们都是“东西”。

比 “你真不是个人” 更狠的骂法也有答案了:“东西”类型远比“人类”更加根源,所以,骂“你真不是个东西”的杀伤力要远大骂“你真不是个人”。

用英文的话,世间万事万物的共同类型就是“Object”,Object即有事物、东西、对象的含义,为了让C++程序听起来高雅一些,中文世界的计算机前辈们选择了“对象”这个翻译。

原来如此,所谓的面向对象编程,原来就是面向东西编程而已……

别胡思乱想了,集中精力,来看超超超级重要的代码:东西类,也就是对象类型,应该如何定义:

struct Object { /* 一片虚空 */ };

在某些语言里,你可能当不了“上帝”,因为那些语言会直接内置 Object 类型,你将因此而丧失实现自行定义世间万物最根源的类型的权利。

花括号中仍然只需保持一片空白即可。我们加了一句注释,是因为此刻它虚空得理直气壮:为对象类型添加任何组成成员,就是对世间的所有东西添加了某种约束——而一旦你添加了任何约束,就必然会造成世间万物中,可能会有某一种东西,正好不满足这个约束,于是这种东西就再属于“东西”……

如果你还不懂得“约束”的话,很可能是因为你还年轻,还没接受过社会的毒打,让我们改为定义爱情是什么吧……

sturct Love { /* 爱情需要由什么组成呢? */ };

把这个定义交给你的女朋友,相信她会抱怨:纸不够长,写不完我对爱情的定义:180平的房,1米8的身高,180万的存款……

定义类型,就是在定义约束;因为:类型即约束。

有了类型,接下来我们可以为类型创造出对象。和平常定义变量并没有区别。事实上,对象也是变量(或常量),只是,基础类型的数据习惯称为变量,而复合类型的变量习惯称为对象

请完成以下例程:

struct Object { }; struct DinosaurPig { }; struct DinosaurSnake { }; int main() { Object o; DinosaurPig pig; DinosaurSnake snake; }

六、实战构造与析构

上面的程序运行后,屏幕输出也将“一片虚空”,尽管身为“上帝”,我们心里清楚曾经有一个东西、一头猪、一条蛇来到过主函数的世界,只是它们悄悄地来,又悄悄地走了……

要怎么才能实现肉眼可观察的,对象的生和死呢?

前面我们说过,采用面向对象的思想编程时,我们就是在为特定类型的事找特定的“人才”,那么,定义一个新的复合类型的目的,本质就是要培养出某一种“类型”的“人才”。

这就要引入“成员函数(member function)”的概念。

为了让特定类型的“人才”具备特定的功能,我们仍然需要写函数——毕竟函数代表做事情——只是,此时我们所写的函数紧密地绑定到某一种类型身上,这种函数在C++中称为类型的成员函数。在更多的面向对象语言里,通常称为“方法/method”,即类型的方法。

让我们还是费点脑细胞,为前面的 “恐猪” 类型,想象一个“方法”吧,比如说 “拱”,那么,“DinosaurPig” 就可以拥有一个方法(成员函数)叫做 “RootAround()”。注意,这个“拱”是特属于恐猪的,和家猪的“拱”是各自的拱法。

C++是个有趣的语言,除了可以给特定类型(的对象)做某事的特定的方法以外,它还允许我们为特定类型(的对象)定制生或死的过程。

定制类型的生的过程,是一种特殊的方法(成员函数),叫构造函数(Constructor),定制类型的死的过程,则是析构函数(Destructor)。

成员函数和普通函数在定义语法上,存在不同。因为成员函数和特定类型紧密相关,所以它可以直接写在类型的内部,即复合类型的那一对花括号内。

构造函数和析构函数又是特殊的成员函数,它们都不能带函数的返回值类型声明,注意,不是指函数返回 void ,而是就不能写返回值类型。

构造函数和析构函数都不能写返回值类型,这显然是一种规定。可以按如下方法,理解这个规定(尽管并不100%正确):

  • 构造函数:负责对象生的过程,其返回结果自然是类的一个新对象,因此无需写返回类型;
  • 析构函数:负责对象死的过程,过程执行后,对象就死了,还有必要有返回值吗?

语义与逻辑上看,Object 类型显然还是应该保持一片虚空才对,只是为了观察它的对象的出生与死亡过程,所以我们才为它添加了构造与析构,且二者都是往屏幕上输出点肉眼可看到的内容。

构造函数和析构函数在命名上也有特殊要求,前者必须和类型名一致,比如 Object(),后者则为类型名字前面加一波浪线,比如:~Object();

#include <iostream> struct Object { Object() { std::cout << "Hello world!" << std::endl; //来自对象的,对世界的问候 } ~Object() { std::cout << "Bye-bye world!" << std::endl; // 对象对世界的告别 } }; // <--- 千万别忘记分号

“恐猪”和“恐蛇”在本课只是在陪太子读书,大家自行发挥它们的生死过程的定制吧。

完成这个程序,你的一只脚已经悄然,迈过了面向对象世界大门的门槛了。