第四章 OPP 中的初始化和无用单元收集


什么是初始化!


● `int main()
{

int i;
int j=10;
i=20;

}`

说明: 根据C++/C 中的定义, i中的值是未定义的, 该值就是创建在 i 的内存区域中所包含的值(在运行栈上, 可能是个垃圾值), 变量 i 未初始化, 变量j代表初始值 10;

注意 : 初始化是在创建变量(或常量)时, 向变量储存已知值得过程。 这意味着该变量被创建(无论以何种方式)时即获得一个值。 进一步而言, 在初始化期间, 为变量初始化时, 我们并未覆盖该变量中的任何值, 换言之, 初始化并不用于擦除变量中的任何现有值。 初始化 只是在变量被创建的同时, 为其储存一个已知值。

如上面的例子, 当我们将20 赋值给i的同时, 正在擦除其中包含的任何值(即使是未知的), 这就是赋值与初始化的不同。

注意 : 赋值一定会擦除变量中的现有值, 变量中的原始值在该步骤中丢失。 初始化是在创建变量的同时便为其储存一个值。 由于被初始化的变量, 在初始化前并不存在, 因此该步骤并未丢失任何值。 任何变量只可以初始化一次, 但可以赋值任意多次。 这是初始化和复制的根本区别。


使用构造函数初始化


● 在C++中, 除非构造函数显式初始化对象的数据成员中的值, 否则该值是未定义的。

就算是在默认情况下, C++ 编译器不会初始化对象的任何数据成员,就算是类的默认构造函数, 也不会为对象的数据成员储存任何预定义的值。

注意 : 一旦创建了类的对象, 客户便可通过该对象调用它所属类的任何成员函数。 实现者不能强制执行规则来限定访问成员函数,而且实现者也不能假定客户通过对象调用成员函数的顺序。

● 注意: 用合适的值初始化对象的所有数据成员。 所谓合适的值, 指的是类的每个成员函数都能清楚解析的值, 类的任何成员函数都必须能理解数据成员中的值, 并且根据该值作出判断。

这里写图片描述


● 对于指针, 0 是一个用于区别指针是否合法的值, 因为合法的指针不会是0地址, 在某些情况下, -1 便可作为整数的特殊值。 例如: 用整数代表数组的下标, 此时将-1 作为特殊的值就特别合适。 数组的下标不可能是负数, 因此-1 表明无效下标。

● 注意 : 在面向对象编程中不用要用Initialize() 函数来初始化对象, 必须用构造函数来初始化。 这很可能导致错误, 因为很可能忘记调用Initialize()

只要当对象依赖于另一个对象进行初始化, 且另一个对象尚未创建时才需要采取这种方式初始化, 在包含虚基类的复杂继承层次中会出现这种情况。


使用内前对象必须遵守的规则


● 如果一个类中包含其他类的对象(成员对象), 那么必须在该类的构造函数中的初始化阶段, 为它所使用的所有内嵌对象调用合适的构造函数,

注意: 在继承时, 对基类成员和成员对象 的初始化必须在 初始化阶段中进行,

● 如果实现者在调用内嵌对象的构造函数时失败, 编译器将设法为内嵌对象调用默认构造函数( 如果有可用且可访问的默认构造函数)

● 如果上面的都不成功, 则构造函数的实现是错误的(导致编译错误)

● 每个内嵌对象的析构函数, 将由包含该对象的类的析构函数自动调用, 无需程序员干预


无用单元


● 所谓无用单元, 是一块存储区(或资源), 该存储区虽然是程序(或进程)的一部分, 但是在该程序中却不可再对其引用。 按照C++的规定, 我们可以说, 无用单元是程序中没有指针指向的某些资源

● 无用单元不会立即对程序造成损害, 但它将逐渐消耗内存, 最终耗尽内存, 导致系统中止运行。 在某些情况下, 由于若干原因, 还可能导致无法停止程序, 随着越来越多的无用单元被创建, 系统的运行得越来越慢。 定期进行无用单元的收集是资源回收的有效途径。

当然, 无用单元收集并不是毫不代价的, 因为必须定期地运行(自动或手动地), 一个收集所有无用单元、并将其返回自由池中的程序, 而且不一定能收集完所有的 无用单元。


悬挂引用


● 当指针所指向的内存被删除, 但程序员认为被删除内存的地址仍有效时, 就会产生悬挂引用,

● 指针别名(即多个指针持有相同的地址)通常会导致悬挂引用。 与无用单元相比, 悬挂引用对程序而言是致命的, 因为它必定导致严重破坏(大多数可能是运行时崩溃)。


无用单元收集和悬挂引用的补救


● 只要不让程序员创建持有内存区域地址的指针类型, 几乎就可以避免悬挂引用的问题。

● C++ 是一种基于值的语言(C也是), 在该语言中, 一切(对象和基本类型)皆为值。 每个对象都是一个真正的对象, 不是一个指向储存在别处的对象的指针。 C++ 对待类和基本类型一样, 这是改语言中的统一模型。


在C++中何时产生无用单元


● 当对象所分配的资源未释放, 但该资源不可再用、不可访问时, 便产生了无用单元。 很多情况下都会导致资源不可访问(或不能使用), 即资源在程序的作用域内不可再用。 例如:

(1) 从函数退出时, 在函数内部创建的所有局部变量(包括对象)以及按值传递的所有参数都不可访问

(2) 从块退出时, 在块内部声明的所有局部变量(包括对象)都不可访问。

(3) 任何复杂表达式包含的临时变量, 在不需要时必须全部予以销毁, 否则它们将成为无用单元。

(4) 任何动态分配的对象, 在不需要时必须由程序员显式地销毁。

注意 : 对象不是简单的变量, 其中甚至还包含其他的对象。 被分配的资源作为对象的一部分, 当该对象不可再用时, 释放已分配的资源(包括其他对象)非常重要。 这是一个递归的过程, 因为对象可能包含其他对象, 而其他对象也可能包含另外的对象,如此以至无限多的对象。

对象内部的所有其他对象所分配的资源, 在不需要时必须及时予以释放。

在复制对象和给对象赋值时, 也会产生无用单元。


C++ 中的无用单元收集


● C++ 中提供类的析构函数专门处理无用单元收集, 但是, 这并不意味着无用单元收集只发生在析构函数中, 实际上, 某些其他成员函数也必须考虑无用单元收集。

● 一个类的析构函数给予对象最后一次释放它所获得的所有资源。 在退出某作用域之前, 由语言自动地为该作用域中创建的自动(基于栈)对象调用析构函数。 此时, 对象即将被销毁(也就说, 被对象占用的内存即将被系统回收)。 一旦析构函数完成, 对象将彻底地消失。

● 删除(使用 delete 操作符) 指向某对象的指针时, 将通过该对象调用对象所属类的析构函数。

● 一般而言, 在对象即将被销毁时, 包含在该对象中的所有对象的析构函数将被递归地调用, 直至所有被包含的对象都被销毁。 这只适用于按值包含在其他对象中的对象。 如果某对象包含指向其他对象的指针, 则由析构函数负责显式销毁它们。


对象的标识


● 在共享对象时, 必须充分理解并正确管理对象的别名。 如果未能正确遵循共享对象的原则, 便会产生悬挂引用、内存泄漏(无用单元)和无法预料的对象状态改变。 悬挂引用在运行时就会暴露出严重的问题, 而内存泄漏则需要时间的积累才会引发问题。

TPerson *person2=new TPerson("10-11-95");

person2不是对象的真正的名称, 它表示内存中另外创建的一个无名称的对象。 在涉及person2 所表示的对象时, 我们可以通过*person2间接地表示该对象名,person2 是指向内存中匿名对象的指针。

● 对象的标识是该对象区别于其他对象的性质。 那么每个对象都拥有一个独一无二的标识, 在其生存期内绝不会改变。

● 在一个对象的生存期内, 可以通过多个名称引用该对象,但该对象的标识是独一无二的。

注意 : 两个对象的状态可能完全一样, 但是它们并不是相同的对象, 因为它们的标识不同, 它们占用不同的内存, 因而位于不同的内存地址。

● 悬挂引用是一个对象有多个名称(别名) 直接导致的结果,这也有可能引起内存泄漏。

● 在共享对象时, 必须充分地理解并正确管理对象的别名。 如果未能正确遵循共享对象的原则, 便会产生悬挂引用、内存泄漏(无用单元) 和 无法预料的对象状态改变。 悬挂引用在运行时就会暴露出严重的问题, 而内存泄漏则需要时间的积累才会引发问题。


对象复制的语义


● 在许多不同的情况中都需要复制对象。 例如, 当按值传递(和按值返回)参数给函数时,就需要制作对象的副本。 当函数被调用时, 复制操作由语言(编译器)发起, 这是一个隐式进行的操作。

● 一般而言, 深复制操作意味着递归地复制整个对象, 而浅复制则意味着在复制对象的过程中, 源对象和副本之间只有共享状态。

● 当在某类中由指针成员或引用, 那么我们希望为它们所指向的内容分配足够的内存, 然后复制那些数据成员中的内容, 在复制操作完成后, 源对象和母的对象之间不会共享任何东西。 这就是深复制。

注意 : 当类包含任何指针、引用或其他对象时, 使用默认复制操作都很不安全。 不要依赖编译器生成的复制构造函数, 应当编写自己的复制构造函数, 已提供正确的复制操作。

● C++ 是一种基于值的语言, 它采用统一的方式处理对象和基本类型。 在进行复制时, 它把一切都当作值, 仅复制值。


● 注意 :当对象的数据成员指向堆中的值时,可能产生的问题就是内存泄漏。 这是因为当对象被删除时,指向堆中值的指针也随之消失, 如果堆中的值还存在, 那么将造成内存泄漏。如果要避免内存泄漏, 对象应当在销毁之前做好清理工作, 删除与之相关的堆中的值。—— 可以使用析构函数, 它会在对象销毁之前被调用, 以便用于执行必要的清理工作。

* 注意 : 如果不编写自己的析构函数, 则编译器替程序员创建一个默认析构函数, 但它并不尝试释放掉任何数据成员可能指向的堆中的内存。 对于简单的类而言, 这样做通常是没有问题的。*

但是当类中由数据成员指向堆中的值时, 则应当编写自己的析构函数, 以便能在对象消失之前释放与对象相关的堆中内存, 避免内存泄漏。

析构函数没有参数和返回值。


● 注意 : 默认拷贝构造函数只是简单地将每个数据成员的值复制给新对象中的同名数据成员, 即按成员逐项进行复制。 当类中含有指向堆中值的数据成员时, 则应当考虑编写自己的拷贝构造函数。

因为当某个对象, 它有一个指针数据成员指向堆中的匿名对象。 如果只用默认拷贝构造函数, 对象的自动复制将会导致新的对象指向堆中的同一个匿名对象, 因为新对象的指针仅仅获得存储在原始对象的指针中地址的一份副本。

这种成员逐项进行的复制造成了浅拷贝, 即副本对象的指针数据成员与原始对象的指针数据成员指向同一内存块。

● 拷贝构造函数不返回值, 但接受一个对类的对象的引用, 该对象就是需要复制的对象。 引用最好声明为常量引用, 以确保原始对象在复制时不被修改。

拷贝构造函数的作用是将原始对象中的任何数据成员复制到目标对象中, 如果原始对象的数据成员是指向堆中值的指针, 则拷贝构造函数应当向内存堆请求分配内存, 然后将原始的堆中的值复制到新的内存块中, 最后让恰当的目标对象的指针数据成员指向新的内存。


对象赋值的语义


● 在C++ 中, 绝大多数的复制操作都由语言隐式调用(当对象按值传递或按值返回时)。当通过现有对象创建新对象时, 也进行了复制操作(但不是很频繁)。 与复制相反的是, 赋值是必须由程序员显式调用的操作。

● 默认赋值操作指的是 系统默认的赋值运算符执行的是 逐个成员赋值

● 注意 : 如果某类中的数据成员是 const成员, 那么在赋值运算符中不允许为其赋值。

● 在 C++ 中, 很容易控制对象的赋值和复制, 如果我们希望禁止公有客户和派生类复制对象, 只需将复制构造函数设置成 private。 对于赋值操作符也一样, 还需注意, 可以限制(而不是完全禁止)制作的副本数目。


左值操作赋值


● 左值就是可以修改的值(通常在赋值操作符左侧的名称), 默认赋值语义会产生一个左值, 这意味着可以进行联级赋值操作(即, a=b=c);

● 记住 :

(1) C++ 允许实现者定义复制对象的语义
(2) C++ 允许实现者定义赋值的语义。
(3) 实现者可以在每个类的基础上控制复制 和赋值语义。


对象相等的语义


● 当我们说两个对象相等时, 要牢记: 两个不同对象相等和两个名称代表相同对象(也就是对象之间等价的概念), 这两个概念是有区别的。

对象相等要比较对象的结构和状态, 而对象等价则要比较 对象的地址。 两个不同的对象可能相等, 但是不允许它们是同一个对象。

● 如果类实现了 == , 则最好也实现 !=, 成对实现操作符可以保证在比较对象时, 两操作符中只有其中之一为真。 如果缺少一个,类的接口则看起来就不完整, 而且即使使用另一个操作更加切合实际,客户也只能被迫使用类所提供的不成对的操作符。

记住 :

如果对象需要比较语义, 要实现 == 操作符

如果实现 == 操作符, 记住要实现 != 操作符

还需注意, == 操作符可以是虚函数, 而 != 操作符通常是非虚函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值