《c++pdf课件第4章初始化与清除》由会员分享,可在线阅读,更多相关《c++pdf课件第4章初始化与清除(14页珍藏版)》请在金锄头文库上搜索。
1、下载 第4章初始化与清除 第2章利用了一些分散的典型 C语言库的构件,并把它们封装在一个 s t r u c t中,从而在库的 应用方面做了有意义的改进。 (从现在起,这个抽象数据类型称为类) 。 这样不仅为库构件提供了单一一致的入口指针,也用类名隐藏了类内部的函数名。在第 3 章中,我们介绍了存取控制(隐藏实现) ,这就为类的设计者提供了一种设立界线的途径,通 过界线的设立来决定哪些是用户可以处理的,哪些是禁止的。这意味着数据类型的内部机制 对设计者来说是可控的和能自行处理的。这样让用户也清楚哪些成员是他们能够使用并加以 注意的。 封装和实现的隐藏大大地改善了库的使用。它们提供的新的数据类型
2、的概念在某些方面比 从C中继承的嵌入式数据类型要好。现在 C + +编译器可以为这种新的数据类型提供类型检查, 这样在使用这种数据类型时就确保了一定的安全性。 当然,说到安全性,C + +的编译器能比C编译器提供更多的功能。在本章及以后的章节中, 我们将看到许多C + +的另外一些性能。它们可以让我们程序中的错误暴露无遗,有时甚至在我 们编译这个程序之前,帮我们查出错误,但通常是编译器的警告和出错信息。所以我们不久就 会习惯:在第一次编译时总听不到编译器那意味着正确的提示音。 安全性包括初始化和清除两个方面。在 C语言中,如果程序员忘记了初始化或清除一个变 量,就会导致一大段程序错误。这在一个
3、库中尤其如此,特别是当用户不知如何对一个 s t r u c t 初始化,甚至不知道必须要初始化时。 (库中通常不包含初始化函数,所以用户不得不手工初 始化s t r u c t) 。清除是一个特殊问题,因为 C程序员一旦用过了一个变量后就把它忘记了,所以 对一个库的s t r u c t来说,必要的清除工作往往被遗忘了。 在C + +中,初始化和清除的概念是简化类库使用的关键所在,并可以减少那些由于用户忘 记这些操作而引起的许多细微错误。本章就来讨论C + +的这些特征。 4.1 用构造函数确保初始化 在s t a s h和s t a c k类中都曾调用i n i t i a l i z e
4、 ( )函数,这暗示无论用什么方法使用这些类的对象, 在使用之前都应当调用这一函数。很不幸的是,这要求用户必须正确地初始化。而用户在专注 于用那令人惊奇的库来解决他们的问题的时候,往往忽视了这些细节。在 C + +中,初始化实在 太重要了,所以不能留给用户来完成。类的设计者可以通过提供一个叫做构造函数的特殊函数 来保证每个对象都正确的初始化。如果一个类有构造函数,编译器在创建对象时就自动调用这 一函数,这一切在用户使用他们的对象之前就已经完成了。对用户来说,是否调用构造函数并 不是可选的,它是由编译器在对象定义时完成的。 接下来的问题是这个函数叫什么名字。这必须考虑两点,首先这个名字不能与类的
5、其他成 员函数冲突,其次,因为该函数是由编译器调用的,所以编译器必须总能知道调用哪个函数。 S t r o u s t r u p的方法似乎是最容易也是最符合逻辑的:构造函数的名字与类的名字一样。这使得 这样的函数在初始化时自动被调用。 下面是一个带构造函数的类的简单例子: 现在当一个对象被定义时: 这时就好像a是一个整数一样:为这个对象分配内存。但是当程序执行到 a的定义点时,构 造函数自动被调用,因为编译器已悄悄地在 a的定义点处插入了一个 X : : X ( )的调用。就像其他 成员函数被调用一样。传递到构造函数的第一个参数(隐含)是调用这一函数对象的地址。 像其他函数一样,我们也可以通
6、过构造函数传递参数,指定对象该如何创建,设定对象初 始值等等。构造函数的参数保证对象的所有部分都被初始化成合适的值。举例来说:如果类 t r e e有一个带整型参数的构造函数,用以指定树的高度,那么我们就必须这样来创建一个对象: tree t(12); / 12英尺高的树 如果t r e e ( i n t )是唯一的构造函数,编译器将不会用其他方法来创建一个对象(在下一章我 们将看到多个构造函数以及调用它们的不同方法) 。 关于构造函数,我们就全部介绍完了。构造函数是一个有着特殊名字,由编译器自动为每 个对象调用的函数,然而它解决了类的很多问题,并使得代码更容易阅读。例如在上一个代码 段中,
7、对有些i n i t i a l i z e ( )函数我们并没有看到显式的调用,这些函数从概念上说是与定义分开 的。在C + +中,定义和初始化是同一概念,不能只取其中之一。 构造函数和析构函数是两个非常特殊的函数:它们没有返回值。这与返回值为 v o i d的函数 显然不同。后者虽然也不返回任何值,但我们还可以让它做点别的。而构造函数和析构函数则 不允许。在程序中创建和消除一个对象的行为非常特殊,就像出生和死亡,而且总是由编译器 来调用这些函数以确保它们被执行。如果它们有返回值,要么编译器必须知道如何处理返回值, 要么就只能由用户自己来显式地调用构造函数与析构函数,这样一来,安全性就被破坏
8、了。 4.2 用析构函数确保清除 作为一个C程序员,我们可能经常想到初始化的重要性,但很少想到清除的重要性。毕竟, 清除一个整型变量时需要作什么 ?只需要忘记它。然而,在一个库中,对于一个曾经用过的对 象,仅仅“忘记它”是不安全的。如果它修改了某些硬件参数,或者在屏幕上显示了一些字符, 或在堆中分配了一些内存,那么将会发生什么呢 ? 如果我们只是“忘记它” ,我们的对象就永 远不会消失。在C + +中,清除就像初始化一样重要。通过析构函数来保证清除的执行。 析构函数的语法与构造函数一样,用类的名字作函数名。然而析构函数前面加上一个, 以和构造函数区别。另外,析构函数不带任何参数,因为析构不需任
9、何选项。下面是一个析构 函数的声明: class Y p u b l i c : Y ( ) ; ; 56C + +编程思想 下载 当对象超出它的定义范围时,编译器自动调用析构函数。我们可以看到,在对象的定义点 处构造函数被调用,但析构函数调用的唯一根据是包含该对象的右括号,即使用 g o t o语句跳出 这一程序块(为了与C 语言向后兼容,g o t o在C + +中仍然存在,当然也是为了方便) 。我们应该 注意一些非本地的g o t o语句,它们用标准C语言库中的setjmp() 和l o n g j m p ( )函数,这些函数将 不会引发析构函数的调用。 (这里作一点说明:有的编译器可
10、能并不用这种方法来实现。依赖 那些不在说明书中的特征意味着这样的代码是不可移植的) 。 下例说明了构造函数与析构函数的上述特征: 第4章 初始化与清除57 下载 下面是上面程序的输出结果: 我们可以看到析构函数在包括它的右括号处被调用。 4.3 清除定义块 在C中,我们总要在一个程序块的左括号一开始就定义好所有的变量,这在程序设计语言 中不算少见(P a s c a l中例外) ,其理由无非是因为“这是一个好的编程风格” 。在这点上,我有 自己的看法。我认为它总是给我带来不便。作为一个程序员,每当我需要增加一个变量时我都 得跳到块的开始,我发现如果变量定义紧靠着变量的使用处时,程序的可读性更强
11、。 也许这些争论具有一定的普遍性。在 C + +中,是否一定要在块的开头就定义所有变量成了 一个很突出的问题。如果存在构造函数,那么当对象产生时它必须首先被调用,如果构造函数 带有一个或者更多个初始化参数,我们怎么知道在块的开头定义这些初始化信息呢?在一般的 编程情况下,我们做不到这点,因为 C中没有私有成员的概念。这样很容易将定义与初始化部 分分开,然而C + +要保证在一个对象产生时,它同时被初始化。这可以保证我们的系统中没有 未初始化的对象。C并不关心这些。事实上, C要求我们在块的开头就定义所有变量,在我们 还不知道一些必要的初始化信息时,就要求我们这样做是鼓励我们不初始化变量。 通常
12、,在C + +中,在还不拥有构造函数的初始化信息时不能创建一个对象,所以不必在块 的开头定义所有变量。事实上,这种语言风格似乎鼓励我们把对象的定义放得离使用点尽可能 近一点。在C + +中,对一个对象适用的所有规则,对预定义类型也同样适用。这意味着任何类 的对象或者预定义类型都可以在块的任何地点定义。这也意味着我们可以等到我们已经知道一 个变量的必要信息时再去定义它,所以我们总是可以同时定义和初始化一个变量。 58C + +编程思想 下载 我们可以看到首先是b u f被定义,然后是一些语句,然后x被定义并用一个函数调用对它初 始化,然后y和g被定义。在C中这些变量都只能在块的一开始定义。一般说
13、来,应该在尽可能 靠近变量的使用点定义变量,并在定义时就初始化(这是对预定义类型的一种建议,但在那里 可以不做初始化) 。这是出于安全性的考虑,减少变量误用的可能性。另外,程序的可读性也 增强了,因为读者不需要跳到程序头去确定变量的类型。 4.3.1 for循环 在C + +中,我们将经常看到f o r循环的计数器直接在f o r表达式中定义: 上述声明是一种重要的特殊情况,这可能使那些刚接触C + +的程序员感到迷惑不解。 变量i和j都是在f o r表达式中直接定义的(在C中我们不能这样做) ,然后他们就作为一个变 量在f o r循环中使用。这给程序员带来很大的方便,因为从上下文中我们可以清
14、楚地知道变量 i、 j的作用,所以不必再用诸如 i _ l o o p _ c o u n t e r之类的名字来定义一个变量,以表示这一变量的作 用。 这里有一个变量生存期的问题,在以前这是由程序块的右大括号来确定的。从编译器的角 度来看这样是合理的,因为作为程序员,我们显然想让 i只在循环内部有效。然而很不幸的是, 如果我们用这种方法声明: (无论有没有大括号, )在同一程序块内,编译器将给出一个重复定义的错误,而新的标准 C + +说明书上说,一个在f o r循环的控制表达式中定义的循环计数器只在该循环内才有效,所以 上面的声明是可行的。 (当然,并不是所有的编译器都支持这一点,我们可能
15、会遇到一些老式 风格的编译器。 )如果这种转变引起一些错误的话,编译器会指出,解决起来也很容易。注意, 第4章 初始化与清除59 下载 那些局部变量会屏蔽这个封闭范围中的变量。 我发现一个在小范围内设计良好的指示器:如果我们的一个函数有好几页,也许我们正在 试图让这个函数完成太多的工作。用更多的细化的函数不仅有用,而且更容易发现错误。 4.3.2 空间分配 现在,一个变量可以在某个程序范围内的任何地方定义,所以在这个变量的定义之前是无 法对它分配内存空间的。通常,编译器更可能像 C编译器一样,在一个程序块的开头就分配所 有的内存。这些对我们来说是无关紧要的,因为作为一个程序员,我们在变量定义之
16、前总是无 法得到存储空间。即使存储空间在块的一开始就被分配,构造函数也仍然要到对象的定义时才 会被调用,因为标识符只有到此时才有效。编译器甚至会检查我们有没有把一个对象的定义放 到一个条件块中,比如在s w i t c h块中声明,或可能被g o t o跳过的地方。 下例中解除注释的句子会导致一个警告或一个错误。 在上面的代码中,g o t o和s w i t c h都可能跳过构造函数的调用点,然而这个对象会在后面的 程序块中起作用,这样,构造函数就没有被调用,所以编译器给出了一条出错信息。这就确保 了对象在产生的同时被初始化。 当然,这里讨论的内存分配都是在一个堆栈中。内存分配是通过编译器向下移动堆栈指针 来实现的(这只是相对而言,实际指针值可能增加,也可能减少,这依赖于机器) 。也可以在 堆中分配对象的内存,这将在第1 2章中介绍。 60C + +编程思想 下载 4.4 含有构造函数和析构函数的stash 在前几章的例子中,都有一些很明显的函数对应为构造函数和析构函数:i n i t i a l i z e ( )和 c l e a n u p ( )。