第三章 C++与数据抽象

● 注意 : 允许客户设置对象中的数据成员值得方法, 通常称为设值方法, 用于返回数据成员值的方法称为 获值方法。

● 在C++中, 抽象的基本单元是类。

● 一个设计良好的类绝不会将数据成员包含在public区域(这违反了数据抽象和封装的原则), 该区域只能包含成员函数。

● 成员函数的实现可以访问在类中声明的所有成员(也就是说, 类的成员函数可以访问类作用域内的任何成员)。

● 如果某程序能访问private区域的成员, 它也能访问类中的其区域的成员

● 如果客户能访问protected区域, 也能访问public 区域。

● 注意 : 任何区域都可以包含成员函数和数据成员。 在类的不同区域中声明的任何成员(数据或函数) 将获得相应声明区域的访问规则。

● 构造函数不能是const或static成员函数, 类可以包含任意数量的重载构造函数

● 注意 : 对象只能用构造函数创建,

● 注意: 如果在一个类中未声明任何构造函数, 编译器会为其生成一个默认构造函数(它还是一个内联函数)。 这个生成的默认构造函数不接受任何参数, 且只允许我们创建对象, 但该对象并未初始化。

只要在类中声明了一个构造函数, 编译器就不会在生成默认构造函数, 如果此时你需要一个默认的构造函数, 可以在手动设置一个。

注意 : 不要在一个类中同时声明一个 带有默认值的构造函数 和一个完全没有参数的默认构造函数, 这样在创建对象的时候可能会出现报错。

● 在调用构造函数时, 只创建了一个空对象, 其中的数据成员包含无用单元,就像是未初始化的自动变量, 需要正确的初始它们, 在初始化完毕后, 这个对象中才包含数据成员的数据

● 注意 : 和其他构造函数一样, 在调用复制构造函数时, 新对象中的数据成员不存在有意义的值(它们都包含无用单元,在复制操作完成之后, 就会跟原来的对象完全一样),需要检查传递给复制构造函数的对象中数据成员的值, 并正确地复制这些值。

● 复制构造函数有一个它所属类的参数(引用)。


这里写图片描述


TIntStack s2 = s1;

说明: s2 调用复制构造函数, 并把现有对象s1作为复制操作的源,然后复制给s2, 当赋值操作完成后, 不管谁的操作都不会影响对方。

● 为某个类创建的对象分配的内存也包括其每一个数据成员的内存, 以及编译器需要的其他内部信息。 编译器会控制这种对象的分配和生存期。列如:

T myT;

● 创建对象时发生了以下3个步骤(无论怎样创建):

(1) 编译器需要获得对象所要求的内存数量

(2) 获得 原始对象内存被转换成一个对象,。 这涉及将对象的数据成员放置在正确的位置, 还有可能建立成员函数指针表(涉及虚函数时)等。 这些都在编译器内部进行, 程序员完全不用担心

(3) 最后, 在(1) 和 (2) 都完成后, 编译器通过新创建的对象调用构造函数(由类的实现者提供或编译器生成的默认构造函数)。


在静态创建对象的情况下, 如果内存已在进程数据区或运行时栈中预留, 则只需要完成步骤 (2) 和 (3) 。 如果有动态分配的对象, 编译器会请求new() 操作符为该对象分配内存。
然后, 在新分配的内存上完成步骤(2) 和 (3) 。 最后返回指向该对象的指针,存储在指针变量中,

● 在C++ 中, 每个对象都会获得它在类中声明的非静态(static)数据成员的副本。

无论该类创建了多少个对象, 在程序运行中, 只产生一个副本。可以使用静态数据成员

TIntStack s3 =TIntStack (250);

这个声明表明我们正在请求创建一个TIntStack 对象s3, 而且为了初始化s3, 指定必须创建一个包含250 个 元素的临时 TIntStack 类对象。 临时对象将在s3 创建之后消失,

● 无论何时需要对象的副本, 编译器都会调用对象所属类的复制构造函数。 复制构造函数负责对对象进行有意义且安全的复制。 当一个函数的形参接受一个类对象的时候, 当调用这个函数时, 编译器将会调用该类的复制构造函数制作一个 要传递该对象的副本。


● `void PrintStack (TIntStack thisOne)
{

}
PrintStack(a); //打印`

从main() 中调用PrintStack 时, 将从实参a 初始化形参 thisOne, 该操作将调用复制构造函数。 此时, 编译器先通过目的对象(thisOne)调用复制构造函数, 并为该复制构造函数提供对象a作为源实参。 现在, PrintStack 函数获得了原始对象的副本, 而且对thisOne的任何改动都不会影响 main () 中的原始对象, 因此保证了数据的安全。

如果离开了 PrintStack 函数, 因为局部对象 thisOne 不再作用域内, 因此也应该回收它所占用的内存, 编译器清楚地知道对象本身的大小, 但是, 它并不知道对象中的由指针所指向的某些动态分配的内存, 此时, 析构函数可以派上用场。

无论何时对象离开作用域, 在编译器真正回收该对象所占用的内存之前, 都会通过对象调用析构函数, 析构函数应该释放对象获得的任何资源。

注意 : 虽然析构函数与其他成员函数类似, 但是, 只有当对象在作用域中不可见时, 编译器才会调用析构函数【需要程序员直接调用析构函数的情况非常少见】。 一旦 析构函数执行完毕, 在此作用域内的对象就不能在被访问。

注意 : 析构函数只在函数返回之前(或离开代码块之前) 被调用。 在退出某作用域之前, 编译器通过该作用域内(函数或块) 已创建的对象调用析构函数,

但是, 程序员无法使用这些对象, 因为对象名在作用域退出时不可见, 由于将要被销毁的对象 在此时仍然存在, 且运行良好, 因此, 析构函数可以通过它调用该类的其他成员函数。

注意 : 我们无需担心其他数据成员所占用的空间, 因为编译器知道它们是对象的一部分,会负责处理。 当创建一个类对象 时, 该对象的大小包含了所有数据成员 大小。 编译器不知道指针所指向的内容, 但它知道指针变量本身的大小 。

注意 : 在析构函数执行完毕后, 编译器将进行一些内部清理操作, 以释放对象所占用的内存。


● 指针所指向的对象由new操作符动态分配, 编译器不会自动释放它(即编译器不会通过该对象自动地调用析构函数)。

通过指针调用delete 操作符时, 将发生以下两个步骤 :

(1) 如果该指针是一个指向类的指针(如: TIntStack *ip = new TIntStack), 则通过该指针指向的对象调用该类的析构函数, 此步骤由程序员执行清理工作。

(2) 回收指针引用的内存。


赋值操作符


● 对于任何赋值操作符, 都应注意以下几点:

(1) 确保对象没有自我赋值(如 : a=a)。

(2) 复用被赋值对象中的资源或销毁他。

(3) 从源对象中将待复制的内容复制到目的对象中。

(4) 最后, 返回对目的对象的引用。

a.operator = (b);

说明: 左侧的对象调用成员函数operation=(), 右侧操作数作为 operation=()的参数, 赋值操作右侧的对象中的数据不能被修改, 该操作的副作用是 : 返回对左侧对象的引用。

因为不能修改右侧的对象(我们只能输入右侧对象并输出左侧对象), 所以该对象应作为const实参传递给 operation=() 函数。


● 注意 : a.operator=(a); // 语法上正确, 但是逻辑上错误

这样的表达式可能会通过指针和引用(别名) 直接或间接地发生, 我们的实现必须检查是否出现这种情况, 因此必须核实 源地址 和 目的地址 是否相同 , 如果赋值发生在相同的对象之间, 则不会进行任何操作, 只返回对目的对象的引用, 如果没有自我赋值检查, 可能会导致严重的问题

● `if (k = j )
{
//将j赋值给k,如果结果不为0,执行if体内部的语句,将j赋值给k,完成赋值后, 返回k的值并与0进行比较的

}`


this 指针 和 名称 重整的进一步说明


● 成员函数中的this 指针是指向调用该成员函数的对象, a.Push(i); 通过对象 a 调用Push成员函数, 在Push 成员函数内部。 this 指针 持有 a 对象的地址,以这样的方式, 该成员函数才可以访问对象内的任何数据成员和成员函数,

每个成员函数应该可以通过某种方法访问调用它的对象, 为达到这个目的, this 指针将作为隐藏的参数传递给每个成员函数, 且 this 指针 通常是 成员函数接收的第一个参数。 那么 “this” 指针就是指向 a 对象。 那么 this指针的类型就是该成员函数的类名

注意 : this 指针不能修改, 也不能赋值, 需要在成员函数内修改this 指针的 情况 非常少见。

注意 : 只有在可能在出现混淆的地方, 才显式使用this 限定符, 但是, 在同一语句中, 通过不同的对象多次调用相同的名称会令人困惑。 因此要显式使用this 限定符。

注意 : 什么时候必须使用this指针 ?

当我们希望返回对调用某函数的对象的引用时, 必须 使用 *this, 另一种情况是在重载赋值操作符中, 我们希望获得对象的地址, 也必须显式使用this 名称, 这是最常见的情况


编译器如何实现 const 成员函数


● 编译器如何检测到为数据成员赋值?

这非常简单, 数据成员和成员函数之间唯一的连接就是this指针。 const成员函数必须把调用它的对象当做const 对象, 这也可以通过将this 指针声明为 指向const 的指针轻松做到, 例如:

unsigned HowMany(const TIntStack *this);

根据这个声明, 任何通过指针给对象内部的数据成员赋值都是非法的,因为该this指针是一个指向常量的指针。

注意 : 在同一类中, 可以包含两个相同的函数, 一个是const函数, 另一个是 非const函数, 这完全可行。

注意 : const成员函数不能在它的实现中调用另一个非const成员函数,


类可以包含什么


● 在C++ 中, 类可以包含 :

  1. 基本类型的数据成员(如int和char)
  2. 另一个类的对象
  3. 指向另一个类(或相同类)对象的指针和对另一个类(或相同类)对象的引用
  4. 指向基本类型的指针和基本类型的引用
  5. 静态数据成员
  6. 成员函数(静态和非静态)
  7. 指向另一个类的成员函数指针
  8. 友元类和(或)友元函数的声明
  9. 另一个类的声明(嵌套类, 极少使用的特性)
  10. 11.

类名 、成员函数名、参数类型和文档


● 设计良好的类需要一个文档, 用于描述每个成员函数的用法。 这些都是设计良好的接口的关键。

● 在大多数情况下, 仅通过查看名称, 无法清楚地了解类及其成员函数的用途。 我们必须提供详尽的文档, 其内容包括 :

  1. 类的用途
  2. 预定客户(打算给谁使用)
  3. 它所依赖的类(如果有)
  4. 类的限制是什么
  5. 他期望从客户方面获得什么

● 在多线程系统中, 还要进一步说明在多线程执行的环境中,类是否可用, 这非常重要。

● 再设计和为类编写文档时, 遵循所有的指导原则非常重要。

● 类的设计者(和实现者) 必须非常清楚地说明创建对象的限制条件,


参数的传递模式——客户的角度


● 注意 : 主调函数是发起转移控制权的函数, 被调函数是接受控制权的函数。

void X::f(T arg) // 按值传递(pass by value)

被调函数可以对arg(原始对象的副本)进行读取和写入。 在f() 内改动 arg 不会影响 f() 的主调函数, 因为主调函数已提供原始对象的副本。 这也许是参数传递最佳和最安全的模式, 主调函数和 被调函数 互相保护。 但是, 这种模式也存在缺点 : 要调用复制构造函数复制原始对象, 然后再将原始对象的副本传递给 f(), 而且在退出f()时, 通常还必须通过 arg 调用析构函数。

注意 : 每次调用构造函数后, 迟早都要调用析构函数销魂对象。 析构函数和构造函数的开销很大。

注意 : 在复制大型对象非常耗时 时。 按值传递参数通常不是首选的方案。 f() 不应该在对象(该对象调用f())的数据成员储存arg 的地址(使用this指针), 因为一旦退出函数,arg即被销毁。

void X::f(const T arg ) // 按值传递

通常, 主调函数对这样的参数传递样式都视而不见。 实际上, 主调函数并不关心被调函数如何操作它的副本。 因为那只是个副本, 并不真正的对象。 const 仅 是被调函数对原始对象副本施加的额外限制。


为参数选择正确的传递模式


● 尽可能避免按值传递大型对象, 对于基本类型和小型对象, 可以按值传递。 复制对象的开销很大。 但是, 如果考虑安全问题, 则应该坚持按值传递。

● 注意 : 需要传递对象时, 不要传递指针

void f( T *object )

(1) 指针无法保证它确实指向某对象,它很可能是一个空指针。(但是可以手动检查啊)

(2) 如果被调函数需要将真正的对象作为参数,则传递引用, 而不是传递指针。 这可确保不出现问题, 被调函数无需检查空引用(因为不可能出现这样的情况)。

(3) 如果希望被调函数修改对象中的数组(输出形参), 则传递引用, 但不是对const引用。

● 当传递基本类型参数时, 要坚持使用值参数, 传递指针和传递整数的开销一样, 但是, 按值传递安全。

● 在可以使用默认值参数的地方, 尽量使用默认值参数。 它们不仅包含更多信息, 还有助于理解参数的用途和减少了客户的负担, 客户只需传递较少的参数。

● 尽量对参数和函数使用 const 限定符, 编译器能够识别const 限定符。

● 如果希望在参数中使用多态, 则必须使用 引用或 指针参数, 不能使用按值传递,此规则也适用于返回值。


函数返回值


● 注意 : 绝不返回对局部变量的引用(或指向局部变量的指针)。 一旦离开函数, 局部变量将被销毁, 但在此之后, 引用(或指针)仍然存在, 它依旧引用(或指向)某些已不存在的对象

● 如果返回一个基本类型, 那么按值返回和按引用或指针 返回 ,效率相同, 但是,按值返回安全,易于理解。

● 在某些情况下(如operator+), 无法返回引用,因为函数的结果未知(而且无法提前运算),正确的实现将要求按值返回(在函数中创建一个临时变量),这是实现这种函数的最佳最安全的方法。

● 如果从const函数返回指针(无论何种原因), 应该返回指向数据成员的const指针。若返回指向数据成员的的非const指针, 这将抵消const函数的优点, 编译器会检测出这样的错误。


从函数中返回引用


● 要避免在函数中返回引用(或指针)

● 如果希望从函数中多态返回, 唯一的选择就是返回引用或者指针。


改善性能


● 注意 : 避免制作对象的副本。 复制对象的开销很大(在内存和CPU 时间方面)
避免创建新对象, 设法复用现有对象。 创建和销毁对象开销很大。
在适当的时候使用const引用形参。
使用const成员函数。

尽可能地使用初始化语义(而非赋值)
优先使用指针而不是引用作为数据成员,指针允许惰性求值。而引用不允许

避免在默认构造函数中分配存储区。 要将分配延迟到访问成员时,通过指针数据成员可轻松完成。

用指针数据成员而不是引用和值成员

尽可能地使用引用计数。

通过重新安排表达式和复用对象减少临时对象。

(2) 在编写代码的最初阶段避免使用技巧。

坚持安全第一, 确保不会出现内存泄漏。

在软件开发的早期阶段, 不用担心优化的问题, 基于性能评定, 在关注这个问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值