天天看点

C++面向对象机制基类和派生类虚函数抽象基类访问控制与继承继承中的类的作用域构造函数和拷贝构造函数容器与继承多重继承与虚继承

基类和派生类

基类中使用

virtual

声明函数,显示的说明这是可以进行重载的。子类中在函数的最后添加上

override

关键字显示的说明重载基类的函数,这是可选的。

但是,如果不声明为虚函数,则解析发生在编译时,而不是运行时。这样会导致在声明为基类的引用或者指针时,只能调用基类的函数,即使此处使用了派生类,也无法进行运行时检验!

virtual

只需要在基类中声明一次即可,这样整个继承链中的子类都可以不用声明,而在运行时动态检验。

运行时绑定: 基类的引用或者指针可以隐式地转化成派生类的,这样程序在运行时可以选择具体的功能,这是运行时绑定。

代码实例:

#include <iostream>
using namespace std;

class A {
  public:
    virtual ~A() {}  
    virtual  void print() {
        cout << "I am base class" << endl;
    }
};

class SubA: public A {
  public:
    void print() override {
        cout << "I am sub class" << endl;
    }
};

void test(A &a) {
    a.print();
}

int main() {
    A a;
    SubA sa;
    test(a);
    test(sa);
    return 0;
}
/*
输出结果:
I am base class
I am sub class
*/
           

上述代码如果去掉基类中的

virtual

关键字和子类中的

override

关键字,则输出:

I am base class
I am base class
           

因为此时是在编译期间执行的,而不是运行时检查!!!

基类都应该有虚析构函数,即使无任何操作也应该如此。虚析构函数的作用是保证在发生动态转换后,仍可以正常的执行基类的析构,代码实例:

#include <iostream>
using namespace std;

class Base {
  public:
    virtual ~Base() {
        cout << "delete Base";
    }
    virtual void print() {
        cout << "I am base" << endl;
    }
};

class Derived: public Base {
  public:
    void print() override {
        cout << "I am derived" << endl;
    }
    ~Derived() {
        cout << "delete derived" << endl;
    }
};

void Delete(Base* cls) {
    delete cls;
    cls = nullptr;
}

int main() {
    Derived* derived = new Derived;
    Delete(derived);
    return 0;
}
           

上述代码输出:

delete derived
delete Base
           

但是,如果去掉基类

Base

构造函数中的

virtual

关键字,那么输出:

delete Base
           

也就是说,派生类没有发生析构,这在工程中是非常危险的,会发生很多资源泄露。

如果基类定义了静态成员,则整个继承体系中只有唯一的一个静态成员。不论基类中派生出多少个子类。。但是静态成员除了这个特性之外,其余的访问特性都遵循继承的访问原则。

在定义基类的时候,添加

final

关键字表示该基类不能被继承:

类型转换的时候,基类可以转换成派生类,但是反过来不行!

虚函数

前面说过,只有声明为

virtual

的函数才能执行动态绑定。否则永远都是执行基类的函数,只能在编译期间确定。一旦函数在基类中声明为虚函数,则在所有的子类中都是虚函数。

子类与基类同名但是参数不同的函数不是重写,因此为了保证正确性,最好是在子类要重写的函数声明后添加

override

关键字。

虚函数也有默认实参,而且子类的默认实参可以和基类的默认实参不同,这可以在运行时确定。但是父子最好一致。

抽象基类

抽象基类看成是C++的接口机制,通过函数后添加

=0

实现。一般来说,为了实现动态绑定,我们需要把纯虚函数声明为

virtual

类型的。

#include <iostream>
using namespace std;

class Interface {
  public:
    // 纯虚函数
    virtual void test() = 0; 
};

class A: public Interface {
  public:
    void test() override {
        cout << "ok" << endl;
    }
};

// 如要执行动态绑定,纯虚函数必须声明为virtual
void test(Interface &it) {
    it.test();
}

int main() {
    A a;
    test(a);
    return 0;
}
           

访问控制与继承

访问属性的图表

C++面向对象机制基类和派生类虚函数抽象基类访问控制与继承继承中的类的作用域构造函数和拷贝构造函数容器与继承多重继承与虚继承

友元的关系不能被继承!!

class

默认私有继承,

struct

默认公有继承。

派生类向基类转换的可访问性:

  • 只有D公有地继承B时,用户代码才能使用派生类向基类转换。
  • 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换
  • 如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和友元可以使用D向B的类型转换;如果是私有继承,则不行

继承中的类的作用域

继承关系中,子类的作用域嵌套在基类的作用域中。当一个名字在派生类作用域中无法解析的时候,会向基类作用域中查找。

前面提到过,如果基类的成员函数不声明为

virtual

,则无法执行动态绑定,也就是使用指针或者引用的时候,都是在编译期进行的,只能使用基类的;如果声明为

virtual

,则是在运行期根据指向或者引用的对象来动态决定的;注意到,如果是值传递,则无法执行动态绑定,仍然调用基类的。

如果派生类中成员与基类成员同名,则派生类成员会掩盖基类成员,一般在程序设计的时候,最好不要掩盖基类成员的非

virtual

成员。如果要调用基类成员,需要使用域作用符号。代码实例:

#include <iostream>
using namespace std;

class Base {
  public:
    virtual void print() {
        cout << "I am Base" << endl;
    }
};

class Base1 {
  public:
    virtual void print() {
        cout << "I am Base 1" << endl;
    }
};

class Derived: public Base, public Base1 {
  public:
    void print() override {
        cout << "I am Derived" << endl;
    }
};


int main() {
    Derived d;
    d.print();
    d.Base::print();
    d.Base1::print();
    Base* b = &d;  // 动态绑定
    b->print();
    Base1 &b1 = d; // 动态绑定
    b1.print();
    // 下面是错误的,多重继承没法转化
    // Base b = d;
    // Base1 b = d;
    return 0;
}
/*
输出结果:
I am Derived
I am Base
I am Base 1
I am Derived
I am Derived
*/
           

上述代码的动态绑定同样根据实际情况进行转换,不过无法执行值传递的转换了。

构造函数和拷贝构造函数

基类的析构函数需要声明为

virtual

的,否则在动态绑定后进行析构时,会发生未定义的错误。

基类构造函数先执行,然后再执行派生类的构造函数;派生类的析构函数先执行,然后再执行基类的构造函数。

容器与继承

使用容器存放继承体系的时候,一般使用简介存放的方式,因为不允许在容器中保存不同类型的元素。我们不能把具有继承关系的多种类型的对象直接存放在容器当中。实际中一般使用存放对象(智能)指针的形式来解决问题。

实际例子:

#include <iostream>
#include <vector>
#include <memory>
using namespace std;

class Base {
  public:
    virtual void print() {
        cout << "I am Base" << endl;
    }
};

class Derived: public Base {
  public:
    void print() override {
        cout << "I am Derived" << endl;
    }
};


int main() {
    vector<shared_ptr<Base>>v;
    v.push_back(make_shared<Derived>());
    v.push_back(make_shared<Base>());
    v[0]->print();
    v[1]->print();
    return 0;
}
/*
输出结果:
I am Derived
I am Base
*/
           

多重继承与虚继承

派生类的构造函数的初始值列表将实参分别传递给每个直接基类。其中基类的构造顺序与派生列表中基类出现的顺序一致,但是派生类构造函数初始值列表中基类的顺序无关。

派生类的析构函数只负责清理本身分配的资源,派生类的成员以及基类都是自动销毁的。

#include <iostream>
using namespace std;

class Base1 {
  public:
    Base1() {
        cout << "I am base 1" << endl;
    }
    Base1(int n) {
        cout << "base1: " << n << endl;
    }
    virtual ~Base1() {
        cout << "delete base1" << endl;
    }
};

class Base2 {
  public:
    Base2() {
        cout << "I am base 2" << endl;
    }
    Base2(int n) {
        cout << "base2: " << n << endl;
    }
    virtual ~Base2() {
        cout << "delete base2" << endl;
    }
};

// 多重继承,注意构造顺序有这里的继承顺序决定
class Derived: public Base1, public Base2 {
  public:
    Derived() {
        cout << "I am derived" << endl;
    }
    // 更改了构造列表的顺序,但是构造顺序任然有继承列表的顺序决定
    // 实际工程中,构造顺序尽量与继承顺序一致。
    Derived(int n1, int n2): Base2(n2), Base1(n1)  {   
        cout << "derived: " << n1 << ", " << n2 << endl;
    }
    ~Derived() {
        cout << "delete derived" << endl;
    }
};

int main() {
    Derived* de = new Derived;
    delete de;
    de = nullptr;
    cout << "---------------------" << endl;
    Derived* de1 = new Derived(1, 2);
    delete de1;
    de1 = nullptr;
}
           

输出结果:

I am base 1
I am base 2
I am derived
delete derived
delete base2
delete base1
---------------------
base1: 1
base2: 2
derived: 1, 2
delete derived
delete base2
delete base1
           

可以看出,多重继承中的析构顺序与构造顺序也是相反的。

基类与派生的动态绑定关系在"继承中的类的作用域"一节中介绍,不在赘述。

多重继承时,一个派生类可能会继承多个基类,使用多重继承的方式可以保证基类只出现一次。 在继承前面添加关键字

virtual

即可。

给出一个经典的“菱形”继承关系:

C++面向对象机制基类和派生类虚函数抽象基类访问控制与继承继承中的类的作用域构造函数和拷贝构造函数容器与继承多重继承与虚继承

在菱形继承中,如果不使用

virtual

关键字,那么FinalClass会保存有两个Base,这会造成空间浪费和产生二义性。

正确的继承方式应该是使用virtual方式:

C++面向对象机制基类和派生类虚函数抽象基类访问控制与继承继承中的类的作用域构造函数和拷贝构造函数容器与继承多重继承与虚继承

正确的处理方式为Derived1和Derived2都使用virtual继承方式,下面给出代码实例:

#include <iostream>
using namespace std;

class Base {
  public:
    Base() {
        cout << "I am Base" << endl;
    }
};

class Derived1: virtual public Base {
  public:
    Derived1() {
        cout << "I am Derived 1" << endl;
    }
};

class Derived2: virtual public Base {
  public:
    Derived2() {
        cout << "I am Derived 2" << endl;
    }
};

class FinalClass: public Derived1, public Derived2 {
  public:
    FinalClass() {
        cout << "I am Final" << endl;
    }
};

int main() {
    FinalClass fc;
    return 0;
}
           

代码输出:

I am Base
I am Derived 1
I am Derived 2
I am Final
           

如果我们去掉Derived1和Derived2中的virtual关键字,那么会出现多次继承的后果,Base构造多次,输出下面结果:

I am Base
I am Derived 1
I am Base
I am Derived 2
I am Final