编程语言

img nanyu

第3章 感受(一)——3.7. Hello object 生死版

发表于2008/9/30 12:22:00  2020人阅读

分类: 白话C++

3.7. Hello object 生死版

初涉编程,很多人都听过“OO”这个词,它是“Object Oriented”的缩写,中文翻译成:“面向对象”,更直一点:“以对象为导向”。具体的含义是,分析问题时,将问题牵涉的种种因素,当成一个完整的“对象”加以考虑,

“面向对象”思路带来一些的新特性,其中“封装”是最基本的一点。即错综复杂的因素,首先被分割成两部分:其中一部分因素被限定仅在“对象”内部捣乱;另外一部分则在对象之间捣乱。如此,经过“对象封装”之后,往往就大大地降低了问题的混乱程度。

〖小提示〗:不用着急,如果你不理解“面向对象”

我有一个好消息,一个坏消息,你想先听哪个?

坏消息是:通常,你不可能仅仅通过“学习”来理解“面向对象”。

好消息是:其实你没有必要在学习C++过程中,就完全理解“面向对象”。

我们学习新知识时,往往带着旧知识的经验;但从另一方面说,也往往带着对现有知识不足之处的深刻认识。这就是所谓的“带着问题学习”。

如果,在编程方面,我们既没有经验——比如C++是你的第一门编程语言;也没有任何“问题”——如果你从没有写过5万行代码以上的项目,那可以差不多可以认定你没有疑惑可以针对“面向对象”发问——怎么办?

没有经验?没关系,C++之父Bjarne Stroustrup说过:“Speak C++ Like a Native”。本书在课程安排上,历经4个版本,你看到的这一版,是对这一点的最佳体现。

没有“问题”?一样没关系,把它交给本书,就让本书负责为您“制造问题”吧。

3.7.1. 定义对象类型

我们在“Hello world 交互版”的3.4.3小节中,我们讲到过“数据类型”。我们提出:“Jerry”不能变成“Tom”,因为它们属于不同的“数据类型”。

C++中,预置很多基础数据类型,比如: int 表示整数类型,那么要定义一个整数,在C++中使用以下语句:

int age;

依据名字猜测,这里可能是要使用age来表示一个年龄数据。


〖小提示〗:常用的几种基础数据类型

int : 整数类型

unsigned int 无符号整数类型(即:正整数及0)

bool “布尔”类型,或称为“真假”类型,它的值只有两种:true和false。

char 字符类型。用于表示一个半角字符,一个汉字通常需要至少两个字符表示。而一个英文字母,阿拉伯数字字符,对应一个char值。

float 浮点数,实数(可带小数的数)的一种表示方法。

double 双精度数,比float可表达精度更高的浮点数。

虽然确实可以把age称呼为一个“对象”,但更通常地,在C++中, 像这样的基础类型的数据,我们仅称呼它为“变量”。

“对象”有什么特殊之处呢?别忘了,“对象”在英文中,称为“Object”,而“Object”还有个直白的翻译:“东西”。在现实生活中,一个“年龄”数据它是个东西吗?它不是个东西吗?

我 们还是约定成俗吧。在C++中,通常我们把由用户自行定义的,复杂类型的数据,称为“对象”——这句话最先透露的信息是:在C++中,不仅可以根据C++ 已有的类型,定义数据;我们,也就是程序员,也就这里所说的“用户”,还可自行定义一种类型——从这一点上,程序员和上帝一样伟大,因为我们可以创建“物 种”——不过只是在代码世界。

C++中,可以使用struct关键字来定义一个数据类型,下面的代码,定义一了个空的数据类型,这个新物种的名字,叫做“DinosaurPig”——我知道你不认识这个单词,因为它是新造的,意思是:“恐猪”,恐龙的近亲:

struct DinosaurPig
{

};

虽然用“恐猪”来表示一个崭新的人造物种,那是相当的直观,但在代码中炫耀 自己的渊博的生物知识——尤其是科幻片中听来的——永远不是一个程序员所应该做的。所以我们还是老老实实地,使用“Object”这个词吧。在OO的传统 中,当然我们需要一个“东西”,可是又不知它具体是一个什么“东西”时,我们就称呼它的类型为“Object”。

struct Object
{

};

该代码创建了一个新物种,名为“Object”。叫这个名字,表明现在我们还不准备去考虑创建什么具体的类型,随着“Hello OO”系列课程的推展,这个问题终会解决。

〖危险〗: 分号很重要!

和 if {…} else {…}不同,定义一个struct时,右花括号之后跟着一个分号(;),它看似不起眼,但如果一不小心忘了输入,编译器一定会给你好看。

3.7.2. 创建对象

现在,我们已经有一个新类型,叫做“Object”,下面我们创建该类型的一个对象。

新创建一个控制台应用,项目名称为“HelloOO”。

然后在main.cpp中,在main()函数之前,添加前面定义Object类型的代码,并在main()函数之中,定义一个Object的变量。最后删除由向导生成的,用于输出“Hello World”的一行代码;“Hello World”?那是过去的事了。

完整代码如下:

#include <iostream>

using namespace
std;

005
struct Object
006
{

008 };

int
main()
{

012
Object o;

014
return 0;
}

注意,012行中,对象的名字是小写字母“o”,而不是数字“0”,C++程序中,不允许以数字作为变量的开始字符。

编译、运行程序,什么也没有看到。

012 行代码中,真的什么事也没有发生了吗?我看到一个控制台的世界产生了,然后我们按了任意键,这个世界又悄然而逝……一切是那样平静,只有程序员知道,在这 期间,曾经有个叫“o”的对象,它来了,又去了;它活过,又死去……一切悄无声息,像蚍蜉、像朝露;像秋天的草,像夏日的花;像康桥上的诗人,轻轻地来又 轻轻地去,不带走一丝云彩……(此处删除1024个字节)

C++规定,只要是对象,就有它的生死过程,并且对于同一种类型,它们有相同的生死过程的定义。这个生死过程,都通过函数来表达。“生”的过程,称为“构造函数”;“死”的过程,称为“析构函数”。

如果我们没有定义这两个函数,像上面的例子,那么将由C++编译器自动为这一类型生成构造和析构函数,而C++自动创建的那个版本的函数,典型特征就就是默不做声。

在上例中,对象“o”,是个小人物,然而小人物也可以有它的自己的声音!

3.7.3. 构造函数

先看代码:

struct Object
{

007
Object()
008 {
009 std::cout << "Hello world!" << endl;
010
}
};

第一、 我们在Object类型中,加入一个函数,函数名也叫Object。

第二、 Object()这个函数,没有返回值类型,连“void”都不是。

和类型同名,没有返回值类型(完全不写),是构造函数的长相特征,请牢记。

在对象“出生”时,构造函数被自动执行。因此,定义一个对象,就相当在其间调用到该对象的构造函数。

保存,编译程序,运行结果……噢,也许你会恼怒地发现,“Hello world”又回来了。或许,本小节的标题应该叫做“Hello world 对象篇”。

3.7.4. 析构函数

析构函数名字为类型名前面加一波浪字符;另外也没有返回值类型。

struct Object
{

Object()
{

std::cout << "Hello world!" << endl;
}


012
~Object()
013
{
014
std::cout << "Bye-bye world!" << endl;
015
}
};

在对象“死亡”时,析构函数被自动执行。不过,对象是何时死亡呢?这一点我们后面讲解。

编译、运行代码,运行结果如下:

(图 20 对象o的构造与析构)

3.7.5. 对象生命周期

C++语言中,对象存在两种形式的生死周期。

第一、 语法决定生命周期:创建对象之后,对象什么时候“死”,完全由语法规则决定。规则是:

全局对象:从程序创建时创建,在程序退出时消亡。但如果存在多个全局对象,则C++程序不保证多个全局对象之间谁先创建。

局部对象:局部对象通常位于一个可执行的语句块之中,则局部对象在该语句块结束时消亡。如果同一语句块中有多个局部变量,则遵循“先生后死”的原则。

第二、 程序员决定生命周期:创建对象之后,对象将永远“活着”,除非代码中主动“处死”该对象。

3.7.5.1. 栈对象

通常,应该在代码中尽量避免使用全局对象,本例我们也仅讲解“局部对象”,因此我们先来讲第一种情况:由语法规则负责其生死的局部对象。正好,这就是本例中小人物o的所处的状况。

(图 21对象o所处的语句块,及它的生命周期)

如 图所示,对象o所处的语句块是main()函数的一对{ }中;而它的生命周期则从它创建的位置开始,到语句块结束之间。出了所处的语句块,o就结束它的生命,于是,“析构函数”在死去之前的一刻被调用,对于我 们这个例子,就是在屏幕上看到一行“Byebye world!”。

当程序运行时,所有活动的数据,都被“生活”在内存(包括虚拟内存)的世界中。就像我们的世界有七大洲四大洋一样,C++程序也内存划分成几个段,其中局部变量全部位于“栈数据段”,因此,局部对象也被称为“栈对象”。

学过数据结构的读者,会了解“堆栈”这种结构的特点:它就像是超市里卖冰棍。冰柜存冰棍时,先扔进去的冰棍被压在箱底;后扔进去的,倒是位于顶部。要卖冰棍时,先被卖出的,肯定是当初靠后扔进去的那一批。(这里不考虑“翻箱倒柜”型的客户。)

栈数据段中的对象也如此,先被创建的,后被消毁;后被创建的,先被消毁。下面代码中增加一个Object类型的栈对象:

int main()
{

Object o1;
Object o2;

return
0;
}

编译、运行改动后的程序,运行结果如下:

(图 22 “栈对象”的生死次序(1))

〖现场作业〗:指出o1、o2的生死次序

请指出上图中四行输出分别由哪个对象的哪个函数输出。

 

实验还在继续,我们特意为o1添加一个新的语句块:

int main()
{
{

Object o1;
}


Object o2;

return
0;
}

再次编译、运行程序。

〖现场作业〗:对象生死次序与“语句块”的关系

上面代码的输出内容将是什么?对比本题与前一题,理解代码中新增的一对花括号所起的作用。

3.7.5.2. 堆对象

“堆对象”属于对象生命周期中的第二类,即:“创建对象之后,对象将永远“活着”,除非代码中主动释放该对象。”

  • 创建堆对象

C++明确区分“栈对象”与“堆对象”,从一开始创建对象的语法就有所不同,请对比:

创建栈对象:

Object o;

 

创建堆对象:

Object *o = new Object();

声明、并且创建一个“堆对象”的语法格式为:

类型名称 *对象名称 = new 构造函数();

如果构造函数允许不传参数,则圆括号可以省略。本例中Object的构造函数正是如此。

下面我们让“o1”对象变身为“堆对象”,o2则保持不变。

int main()
{
{

Object *o1 = new Object;
}


Object o2;

return
0;
}

编译、运行程序,输出结果为:

(图 23 o1为堆对象的生死)

屏 幕上唯一的“Bye-bye world!”是“栈对象”o2的临终遗言,它自身自灭,走得从容。而“堆对象”o1呢?程序中尚未写上释放o1的代码。因此,它似乎长生不老了——其实 也不是,在程序退出时,o1将被强行杀死,但那时已经是天崩地裂般的末日,o1将灾难一般地死去,不会有机会去留下它的遗言(即:没有机会执行它的析构函 数)。

我愿用最真诚的心灵,祈祷5.12灾难中死去的人们。

〖重要〗:需要时创建;不需要时释放!

这个重要原则的完整表达是:“仅当需要时,才创建你的对象;一旦不需要,就立即释放你的对象”。例子中的代码,似乎没什么大毛病,但如果反复存在创建而不释放的对象,整个程序就可能会被内存不足的问题而拖跨掉。

  • 释放堆对象

C++中,释放单个堆对象的方法是使用delete关键字:

当然,此时的o必须是一个堆对象。

下面我们加上释放o1的代码:

int main()
{
{

Object *o1 = new Object;
delete
o1;
}


Object o2;

return
0;
}

编译、运行程序,结果如下:

(图 24 delete 主动释放o1对象)

3.7.6. 对象可见区域

我们将前例代码中,delete 一行更换一下位置:

int main()
{

020
{
021
Object *o1 = new Object;
022
}

024
delete o1;
025
Object o2;

return
0;

编译,得到错误:“……/HelloOO/main.cpp|24|error: `o1' was not declared in this scope|”。意思是“o1没有被声明在当前区域内”。

针对021的语句:

021        Object *o1 = new Object;

在C++中,真正存放在“堆内存”中的数据,是*o1的内容。严格地讲o1本身仍然是一个栈数据,*o1才“堆内存”里的对象。只是通常为了不把事情搞得过于细节,才会笼统地称o1为堆对象。

就好像程序在“堆内存”中盖一座楼,但程序却把这座的地址,记在“栈内存”中。每当程序想要访问一下“堆内存”中的那座楼,它就不得不先在“栈内存”中找一找那座楼的地址。如果失去了“栈内存”中所记的地址,程序就再也访问不了那座楼了——虽然,那座楼它一直存在着。

下面的代码,创建一个“无名”的堆对象,它被我们创建了,可是我们却没办法“抓”到它:

new Object;

我们可以狠心一些,在创建之后,立即杀死它。

delete new Object;

显然,这两行代码看上去有些怪异。通常情况下,我们创建一个“堆变量”时,会同步为它创建一个“栈变量”用于记住“堆变量”的地址。

再次看一段代码:

020    {
021
Object *o1 = new Object;
022
}
023
024
delete o1;

当代码运行到第023行时,发生了什么?由于它出了020~022的语句块,因此作为“栈变量”,“o1”已经消亡,真正还存在的是“堆变量”其实是“*o1”。

别忘了,我们说过,程序要访问堆内存中的“*o1”,就必须先在“栈内存”中通过找到“o1”,因为“o1”里存放着“*o1”的地址,然而,在023行时,“o1”已经消亡,所以我们再也访问不了“*o1”了。

所以,“delete o1” 这行代码也就没办法编译过去。再说一次,对“*o1”的一切操作,都必须先找到它的地址,包括delete操作。delete相当是把堆内存的那座楼炸毁掉,然后要炸楼?请先给我楼的确切地址。

下面我们对代码再做一次改进:

int main()
{

Object* o1;

{

o1 = new Object;
}


delete
o1;
Object o2;

return
0;
}

〖现场作业〗:编译并理解以上代码

编译、并运行以上代码,理解为什么它没有产生前一例中的编译错误。

阅读全文
0 0

相关文章推荐

img
取 消
img