跳转至

类的继承

类不同类型的成员访问范围

成员类型/访问范围 public成员 protected成员 private成员
外部 可以 不可以 不可以
继承类 可以 可以 不可以
友元 可以 可以 可以

继承访问关系

成员/继承类型 public成员 protected成员 private成员
public继承 继承为public 继承为protected 不可访问
protected继承 继承为protected 继承为protected 不可访问
private继承 继承为private 继承为private 不可访问

虚继承

一般来说,类继承的内存布局是这样的,从子类开始,内部包含父类,一直到最底层的基类

对菱形继承,这样就会导致两个中间子类里都存一个基类,导致子类直接调用基类的话,会发生混淆

虚继承就是对这样的情况,将基类的两个子类继承时,增加virtual标注,这样最终子类中会单独构造一个基类,而两个中间子类里不会有基类,而是有一个虚基表指针,里面记录中间子类和公共基类的内存地址偏移量。这样子类调用基类就不会存在混淆,同时中间子类也可以明确访问到唯一的公共基类


一个应用例子

工作中碰到过这个例子,一个基类有两个virtual方法,分别有两个实现版本,组合成四个类来使用

为了实现简洁,用两组中间类分别各实现一个方法,然后从两组中各取一个类,组合继承组成最后的子类,这里就需要用虚继承

这个例子中这样就可以完成,因为中间类其实有未实现方法,不会直接构造,不需要用中间类去构造基类

但如果中间类也是可以调用的类,可以再加一个private的Base构造函数方便使用,同时标记中间类为友元,这样只有中间类能访问private的构造函数

#include <iostream>

class Base {
// private:
//     Base() {
//         std::cout << "Base()\n";
//     }
//     friend class A;
//     friend class B;
public:
    virtual void func_1() = 0;
    virtual void func_2() = 0;
    Base(int num) {
        std::cout << "Base(" << num << ")\n";
    }
};

class A : protected virtual Base {
public:
    void func_1() override final {
        std::cout << "func_1 in A\n";
    }
};

class B : protected virtual Base {
public:
    void func_1() override final {
        std::cout << "func_1 in B\n";
    }
};

class C : protected virtual Base {
public:
    void func_2() override final {
        std::cout << "func_2 in C\n";
    }
};

class D : protected virtual Base {
public:
    void func_2() override final {
        std::cout << "func_2 in D\n";
    }
};

class Derived : public A, public C {
public:
    Derived() : Base(10) { // 这里如果写Base()会编译失败
    }
};

int main() {
    Derived d;
    d.func_1();
    d.func_2();
}

类型转换与继承

对于基类和派生类,有一个重要规则: 可以将基类的指针或者引用绑定到派生类对象上

对于一个变量或表达式,存在静态类型动态类型,静态类型在编译期就已经确定,动态类型则在运行时才知道,而基类的指针或引用的动态类型可能与静态类型不一致,也只有指针或引用可以,基类的变量永远是基类的类型,不能更改

派生类可以向基类转化,而基类永远不可能向派生类转换,因为基类只是派生类的一部分,即使是指针和引用也不行,因为编译器编译期间只能通过静态类型来进行检查,如下,即使第二行的基类指针已经和派生类绑定在一起,也无法转换成派生类指针

Derived derived;
Base* basePtr = &derived;
Derived* derivedPtr = basePtr; // 出错

虚函数

以上所述动静态类型和虚函数关系很大,因为当虚函数通过指针或引用被调用时,直到运行时才知道具体的调用结果,所以所有的虚函数都必须有定义,因为编译器无法确定一个虚函数是否会被使用

通过指针或引用调用虚函数时,被调用的是与绑定到指针或引用上的动态类型相匹配的一个

因此基类指针可以调用子类的虚函数

c++11中可以使用override关键字来说明派生类中的虚函数,同时还可以把某个函数指定为final,则之后任何尝试覆盖该函数的操作都将引发错误

作用域 可以通过作用域来强迫调用基类的函数版本而不是动态绑定的虚函数

Base* ptr = new PubDerived();
ptr->Base::virtualFunction(); // 调用的仍然是基类的虚函数

虚析构函数

  • 当基类包含virtual成员函数时,表示该基类可能会用作多态,即基类指针或引用绑定派生类对象,这时就要定义一个虚析构函数,来保证在析构时调用的是子析构函数
  • 子析构函数会后调用父析构函数,相应,子构造函数会先调用父构造函数,以下代码执行结果可见
class Base {
public:
    Base() { cout << "Base Constructor\n"; }
    virtual ~Base() { cout << "Base Destrution\n"; }
};
class PubDerived : public Base {
public:
    PubDerived() { cout << "PubDerived Constructor\n"; }
    ~PubDerived() { cout << "PubDerived Destructoin\n"; }
};
int main() {
    Base* ptr = new PubDerived();
    delete ptr;
}

执行结果为

Base Constructor
PubDerived Constructor
PubDerived Destructoin
Base Destrution

C++拷贝构造

一个构造函数第一个参数是自身类型的引用,且任何额外参数都有默认值,则这个构造函数是拷贝构造函数

class Foo {
public:
    Foo(); // 默认构造函数
    Foo(const Foo&); // 拷贝构造
};

拷贝构造函数必须是引用,可以不定义成const,但这个参数应用中几乎都是const的

如果没有给一个类定义拷贝构造函数,编译器会定义一个

出现情况

  1. 用另一个对象来定义变量
  2. 用=来定义变量
  3. 经一个对象作为实参传递给一个非引用类型的形参
  4. 从一个返回类型为非引用的函数返回一个对象
  5. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

多态

指相同的对象收到不同的消息或者不同的对象收到相同的消息时产生的不同的实现动作

  • 编译时多态性(静态多态):就是在编译期确定的一种多态,在C++中主要体现在函数模板
  • 重载是多态性中最简单的形式,它是指用同一名字表示不同的函数或运算符,从而使C++具有更大的灵活性和扩展性,它分为运算符重载和重载函数两种
  • 运行时多态性(动态多态):通过虚函数实现。具体来说,声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法

只有在通过指针或引用调用虚函数的时候,才会在运行时解析该调用,对象的动态类型才会与静态类型不一致

#include<iostream>
using namespace std;

//基类对象
class Base {
public:
    //有virtual关键字,运行时多态
    virtual void f(float x) {
        cout<<"Base::f(float)"<< x <<endl;
    }
    //无viratul关键字,不会发生运行时多态
    void g(float x) {
        cout<<"Base::g(float)"<< x <<endl;
    }
    void h(float x) {
        cout<<"Base::h(float)"<< x <<endl;
    }
};
class Derived : public Base {
public:
    virtual void f(float x) {
        cout<<"Derived::f(float)"<< x <<endl;   //多态、覆盖
    }
    //子类与父类的函数同名,无virtual关键字,则为隐藏
    void g(int x) {
        cout<<"Derived::g(int)"<< x <<endl;     //隐藏
    }
    void h(float x) {
        cout<<"Derived::h(float)"<< x <<endl;   //隐藏
    }
};
int main(void) {
    Derived d;  //子类
    Base base = d; // Base::f(float)3.14
    base.f(3.14f);
    Base *pb = &d;  //基类指针指向子类
    Derived *pd = &d;   //子类指针指向自己
    // Good : behavior depends solely on type of the object
    pb->f(3.14f);   // Derived::f(float) 3.14   调用子类,多态
    pd->f(3.14f);   // Derived::f(float) 3.14   调用子类

    // Bad : behavior depends on type of the pointer
    pb->g(3.14f);   // Base::g(float)  3.14 无多态,调用自己的
    pd->g(3.14f);   // Derived::g(int) 3    无多态,调用自己的

    // Bad : behavior depends on type of the pointer
    pb->h(3.14f);   // Base::h(float) 3.14  无多态,调用自己的
    pd->h(3.14f);   // Derived::h(float) 3.14   无多态,调用自己的
    return 0;
}