C++编程思想笔记之六

类别:编程语言 点击:0 评论:0 推荐:

第13章  继承和组合

一、继承和组合

组合:简单地创建一个包含已存在的类对象的新类,这称为组合,因为这个新类是由已存在类的对象组合的。对于新类的public接口函数,包含对嵌入对象的使用,但不必模仿这个嵌入对象的接口。

继承:创建一个新类作为一个已存在类的类型,采取这个已存在类的形式,对它增加代码,但不修改它。

二、构造函数和析构函数的次序

构造在类层次的最根处开始,而在每一层,首先调用基类构造函数,然后调用成员对象构造函数。调用析构函数则严格按照构造函数相反的次序—这是很重要的,因为要考虑潜在的相关性。对于成员对象,构造函数调用的次序完全不受在构造函数
的初始化表达式表中次序的影响。该次序是由成员对象在类中声明的次序所决定的。

三、非自动继承的函数

构造函数和析构函数是用来处理对象的创建和析构的,它们只知道对在它们的特殊层次的对象做什么。所以,在整个层次中的所有的构造函数和析构函数都必须被调用,也就是说,构造函数和析构函数不能被继承。operator= 也不能被继承,因为它完成类似于构造函数的活动。

四、组合与继承的选择

组合通常在希望新类内部有已存在类性能时使用,而不希望已存在类作为它的接口。这就是说,嵌入一个计划用于实现新类性能的对象,而新类的用户看到的是新定义的接口而不是来自老类的接口。

因为正在由一个已存在的类做一个新类,并且希望这个新类与已存在的类有严格相同的接口(希望增加任何我们想要加入的其他成员函数),所以能在已经用过这个已存在类的任何地方使用这个新类,这就是必须使用继承的地方。

五、对私有继承成员公有化


第14章  多态和虚函数

C++如何实现晚捆绑

编译器对每个包含虚函数的类创建一个表(称为VTABLE)。在VTABLE中,编译器放置特定类的虚函数地址.在每个带有虚函数的类中,编译器秘密地置一指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个VPTR,并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置VTABLE、初始化VPTR、为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。利用虚函数,这个对象的合适的函数就能被调用,哪怕在编译器还不知道这个对象的特定类型的情况下。

 

如果没有数据成员, C + +编译器会强制这个对象是非零长度,因为每个对象必须有一个互相区别的地址。如果我们想象在一个零长度对象的数组中索引,我们就能理解这一点。一个“哑”成员被插入到对象中,否则这个对象就有零长度。

 

C + +对此提供了一种机制,称为纯虚函数。下面是它的声明语法:
virtual void x() = 0;
这样做,等于告诉编译器在V TA B L E中为函数保留一个间隔,但在这个特定间隔中不放地址。只要有一个函数在类中被声明为纯虚函数,则V TA B L E就是不完全的。包含有纯虚函数的类称为纯抽象基类。如果一个类的V TA B L E是不完全的,当某人试图创建这个类的对象时,编译器做什么呢?由于它不能安全地创建一个纯抽象类的对象,所以如果我们试图制造一个纯抽象类的对象,编译器就发出一个出错信息。这样,编译器就保证了抽象类的纯洁性,我们就不用担心误用它了。

纯虚函数防止产生V TA B L E,但这并不意味着我们不希望对其他函数产生函数体。我们常常希望调用一个函数的基类版本,即便它是虚拟的。把公共代码放在尽可能靠近我们的类层次根的地方,这是很好的想法。这不仅节省了代码空间,而且能允许使改变的传播变得容易。

对象切片:

当多态地处理对象时,传地址与传值有明显的不同。所有在这里已经看到的例子和将会看到的例子都是传地址的,而不是传值的。这是因为地址都有相同的长度,传派生类型(它通常稍大一些)对象的地址和传基类(它通常小一点)对象的地址是相同的.如果使用对象而不是使用地址或引用进行向上映射,发生的事情会使我们吃惊:这个对象被“切片”,直到所剩下来的是适合于目的的子对象。

#include <iostream>
using namespace std;

class base
{public:
    base(int  j)
 {
  cout<<"base()"<<endl;
  i=j;
 }
 base (base & b)
 { cout<<"base(base&)"<<endl;
   i=100;
 }
 virtual  int sum() {return i;};
private :
 int i;
};

class derive :public base
{
public:
 derive(int m,int k):base(k)
 {
    cout<<"derive()"<<endl;
    j=m;
 }
 int sum()
 {
  return(base::sum()+j);
 }
private:
 int j;
};

void call( base b)
{
 cout<< b.sum();
}

main()
{

 base   a(1);
 derive b(1,2);
 call(b);

}

 

三、构造函数调用次序
所有基类构造函数总是在继承类构造函数中被调用。因为构造函数有一项专门的工作:确保对象被正确的建立。派生类只访问它自己的成员,而不访问基类的成员,只有基类构造函数能恰当地初始化它自己的成员。如果不在构造函数初始化表达式表中显式地调用基类构造函数,它就调用缺省构造函数。如果没有缺省构造函数,编译器将报告出错。当继承时,我们必须完全知道基类和能访问基类的任何public和protected成员。这也就是说,当我们在派生类中时,必须能肯定基类的所有成员都是
有效的。在通常的成员函数中,构造已经发生,所以这个对象的所有部分的成员都已经建立。然而,在构造函数内,必须想办法保证所有我们的成员都已经建立。保证它的唯一方法是让基类构造函数首先被调用。这样,当我们在派生类构造函数中时,在基类中我们能访问的所有成员都已经被初始化了。在构造函数中,“必须知道所有成员对象是有效的”也是下面做法的理由:只要可能,我们应当在这个构造函数初始化表达式表中初始化所有的成员对象(放在合成的类中的对象)。只要我们遵从这个做法,我们就能保证所有基类成员和当前对象的成员对象已经被初始化。

第15章  模板和包容器

一、包容器所有权问题

一个包容器所包含的指针所指向的对象都仅仅用于包容器本身。在这种情况下,所有权问题简单而直观:该包容器拥有这些指针所指向的对象。由于通常大多数都是上述情况,因此把包容器完全拥有包容器内的指针所指向的对象的情况定义为缺省情形。处理所有权问题的最好方法是由用户程序员来选择。这常常用构造函数的一个参数来完成,它缺省地指明所有权(对于典型理想化的简单程序)。另外还有读取和设置函数用来查看和修正包容器的所有权。假若包容器内有删除对象的函数,包容器所有权的状态会影响删除,所以我们还可以找到在删除函数中控制析构的选项。我们可以对在包容器中的每一个成员添加所有权信息,这样,每个位置都知道它是否需要被销毁,这是一个引用记数变量,在这里是包容器而不是对象知道所指对象的引用数。

本文地址:http://com.8s8s.com/it/it29622.htm