C++回顾——继承和组合

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ZLANBL085321/article/details/81943546

一、组合语法
实际上,我们一直在用组合创建类,只不过是在用内部数据类型或已存在类的对象组合新类。

二、继承语法
在代码中和原来一样给出该类的名字,但在类的左括号的前面,加一个冒号和基类的名字(对于多重继承,要给出多个基类名,它们之间用逗号分开)。当做完这些时,将会自动地得到基类中的数据成员和成员函数。
在继承时,基类中所有的成员都是被预设为私有的,所以如果基类的前面没有public,这意味着基类的所有公有成员将在派生类中变为私有的;而继承时通过使用关键字public,则基类中的所有公有成员在派生类中仍是公有的。
派生类可以直接访问所有基类的公有函数;如果派生类中重写了基类的函数,将会使用派生类的重定义版本,如果仍想调用基类的函数,则必须使用作用域运算符来显式地标明基类名。

三、构造函数的初始化表达式表
构造函数的初始化表达式表的形式模仿继承活动,将子对象构造函数的调用语句放在构造函数参数表和冒号之后,在函数体的左括号之前,如果在初始化表达式表中有多个构造函数的调用,则用逗号隔开。
构造函数的初始化表达式表允许我们显式地调用成员对象的构造函数,它的主要思想是:在进入新类的构造函数体之前调用所有其他的构造函数,这样,对子对象的成员函数所做的任何调用总是转到了这个初始化的对象中。即使编译器可以隐藏地调用默认的构造函数,但在没有对所有的成员对象和基类对象的构造函数进行调用之前,就没有办法进入该构造函数体。这是C++的一个强化机制,它确保了如果没有调用对象的构造函数,就别想向下进行。
为了使语法一致,可以把内部类型看做只有一个取单个参数的构造函数,而这个参数与正在初始化的变量的类型相同。但要记住,这些并不是真正的构造函数,如果没有显式地调用伪构造函数,初始化是不会执行的。如下:

class X {
    int i;
    float f;
    char c;
public:
    x() : i(1), f(1.4), c('x') {}
};

构造是从类层次的最根处开始,而在每一层,首先会调用基类构造函数,然后调用成员对象构造函数(对于成员对象,构造函数调用的次序完全不受构造函数的初始化表达式表中的次序影响,该次序是由成员对象在类中声明的次序所决定的,否则就会对两个不同的构造函数有两种不同的调用顺序,而析构函数将不知道如何相应逆序执行析构,这就产生了相关性问题)。调用析构函数则严格按照构造函数相反的次序。

四、名字隐藏
如果继承一个类并且对它的成员函数重新进行定义,可能会出现两种情况:1)正如在基类中所进行的定义一样,在派生类的定义中明确地定义操作和返回类型,这称为对普通成员函数的重定义;2)如果基类的成员函数是虚函数,则称为重写。
任何时候如果重新定义了基类中的一个重载函数,在新类之中所有其他的版本则被自动地隐藏了。如果通过修改基类中一个成员函数的操作或返回类型来改变了基类的接口,我们就没有使用继承所提供的功能,而是按另一种方式来重用了该类(由于继承的最终目的是为了实现多态性,如果我们改变了函数特征或返回类型,实际上便改变了基类的接口)。

五、非自动继承的函数
不是所有的函数都能自动地从基类继承到派生类中的。构造函数和析构函数用来处理对象的创建和析构操作,但它们只知道对它们的特定层次上的对象做些什么,所以,构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。
operator=也不能被继承,因为它完成类似于构造函数的活。除了赋值运算符以外,其余的运算符可以自动地继承到派生类中。
在继承过程中,如果不亲自创建这些函数,编译器就会生成它们。被生成的构造函数使用成员方式的初始化,被生成的operator=使用成员方式的赋值,生成的operator=仅仅作用于同种类型对象。如果想把一种类型赋于另一种类型,则必须自己写operator=。
一旦决定写自己的拷贝构造函数和赋值运算符,编译器就会假定我们已制定所做的一切,并且不再像在生成的函数中那样自动地调用基类版本。如果想调用基类版本,就必须显式地调用它们。

静态成员函数和非静态成员函数的共同点:
1)它们均可被继承到派生类中;
2)如果我们重新定义了一个静态成员,所有在基类中的其他重载函数会被隐藏;
3)如果我们改变了基类中的一个函数的特征,所有使用该函数名字的基类版本都将会被隐藏。
不同点:
静态成员函数不能是虚函数。

六、组合与继承的选择
组合通常是在希望新类内部具有已存在类的功能时使用,而不是希望已存在类作为它的接口。也就是说,嵌入一个对象用以实现新类的功能,而新类的用户看到的是新定义的接口而不是来自老类的接口。
希望新类与已存在的类有着严格相同的接口,能在已经用过这个已存在类的任何地方使用这个新类,这就必须使用继承。
通过在基类表中去掉public或通过显式地声明private,可以私有地继承基类。创建的新类具有基类的所有数据和功能,但这些功能是隐藏的,该类的用户访问不到这些内部功能,并且新类的对象不能看做是这个基类的实例。通常情况不使用private继承,偶然有这种情况(可能想产生像基类接口一样的接口部分,而不允许该对象的处理像一个基类对象,private继承提供了这个功能)。
当私有继承时,基类的所有public成员都变成了private,如果希望它们中的任何一个是可视的,在派生类的public部分声明它们的名字即可,例如:

class A{
public:
int number() const { return 100; }
float fnumber() const { return 1.0; }
float fnumber(int) const { return 2.0; }
};

class B : A {
public:
    using A::number;
    using A::fnumber; // both memebers exposed
};

这样,如果想要隐藏基类的部分功能,则private继承是有用的,如果给出一个重载函数的名字将使基类中所有它的重载版本公有化。

在实际项目中,有时希望某些东西隐藏起来,但仍允许其派生类的成员访问,此时protected就派上了用场,它的意思是“就这个类的用户而言,它是private的,但它可被从这个类继承来的任何类使用”。最好让数据成员是private,因为我们应该保留改变内部实现的权利,然后才能通过protected成员函数控制对该类的继承者的访问。
保护继承的派生类意味着对其他类来说是“照此实现”,但它对于派生类和友元是“is-a”,它是不常用的。

确定应当用组合还是继承,最清楚的方法之一是询问是否需要从新类向上类型转换。

七、向上类型转换
继承最重要的方面不是它为新类提供了成员函数,而是它是基类与新类之间的关系,这种关系可被描述为:“新类属于原有类的类型”。从派生类到基类的类型转换,称为向上类型转换(upcasting),这种转换是安全的(从更专门的类型到更一般的类型)。
如果允许编译器为派生类生成拷贝构造函数,它将首先自动地调用基类的拷贝构造函数,然后再是各成员对象的拷贝构造函数(或在内部类型上执行位拷贝),因此可以得到正确的操作。然而,如果自己写派生类的拷贝构造函数,并且出现错误,这时将会为派生类的基类部分调用默认的构造函数,这是在没有其他的构造函数可供选择调用的情况下,编译器回溯搜索的结果。所以在创建自己的拷贝构造函数时,要正确地调用基类拷贝构造函数(这是向上类型转换的一种情况)。
向上类型转换还能出现在对指针或引用简单赋值期间。任何向上类型转换都会损失对象的类型信息。

展开阅读全文

没有更多推荐了,返回首页