玩命加载中 . . .

抽象类/虚函数/虚继承


一、多态性

多态性:多态就是在同一个类或继承体系结构的基类与派生类中,用同名函数来实现各种不同的功能。

静态绑定又称静态联编,是指在编译程序时就根据调用函数提供的信息,把它所对应的具体函数确定下来,即在编译时就把调用函数名与具体函数绑定在一起。

动态绑定又称动态联编,是指在编译程序时还不能确定函数调用所对应的具体函数,只有在程序运行过程中才能够确定函数调用所对应的具体函数,即在程序运行时才把调用函数名与具体函数绑定在一起。

编译时多态性:静态联编(连接)——系统在编译时就决定如何实现某一动作,即对某一消息如何处理。静态联编具有执行速度快的优点。在C++中的编译时多态性是通过函数重载和运算符重载实现的。
运行时多态性:动态联编(连接)——系统在运行时动态实现某一动作,即对某一消息在运行过程实现其如何响应。动态联编为系统提供了灵活和高度问题抽象的优点,在C++中的运行时多态性是通过继承和虚函数实现的。

二、虚函数

虚函数的意义

1、基类与派生类的赋值相容

  • 派生类对象可以赋值给基类对象。
  • 派生类对象的地址可以赋值给指向基类对象的指针。
  • 派生类对象可以作为基类对象的引用。

赋值相容的问题:不论哪种赋值方式,都只能通过基类对象(或基类对象的指针或引用)访问到派生类对象从基类中继承到的成员, 不能借此访问派生类定义的成员。

2、虚函数使得可以通过基类对象的指针或引用访问派生类定义的成员。

3、virtual关键字其实质是告知编译系统,被指定为virtual的函数采用动态联编的形式编译。

4、虚函数的虚特征:基类指针指向派生类的对象时,通过该指针访问其虚函数将调用派生类的版本。

  • 一旦将某个成员函数声明为虚函数后,它在继承体系中就永远为虚函数了
  • 如果基类定义了虚函数,当通过基类指针或引用调用派生类对象时,将访问到它们实际所指对象中的虚函数版本。
  • 只有通过基类对象的指针和引用访问派生类对象的虚函数时,才能体现虚函数的特性。
  • 派生类中的虚函数要保持其虚特征,必须与基类虚函数的函数原型完全相同,否则就是普通的重载函数,与基类的虚函数无关。
  • 派生类通过从基类继承的成员函数调用虚函数时,将访问到派生类中的版本。
  • 只有类的非静态成员函数才能被定义为虚函数,类的构造函数和静态成员函数不能定义为虚函数。原因是虚函数在继承层次结构中才能够发生作用,而构造函数、静态成员是不能够被继承的。
  • 内联函数也不能是虚函数。因为内联函数采用的是静态联编的方式,而虚函数是在程序运行时才与具体函数动态绑定的,采用的是动态联编的方式,即使虚函数在类体内被定义,C++编译器也将它视为非内联函数。

5、基类析构函数几乎总是为虚析构函数。
假定使用delete和一个指向派生类的基类指针来销毁派生类对象,如果基类析构函数不为虚,就如一个普通成员函数,delete函数调用的就是基类析构函数。在通过基类对象的引用或指针调用派生类对象时,将致使对象析构不彻底!

三、纯虚函数和抽象类

1、纯虚函数概念?

仅定义函数原型而不定义其实现的虚函数
实用角度:占位手段place-holder
方法学:接口定义手段,抽象表达手段

class X
{
    virtual ret_type func_name (param) = 0;
}

2、抽象类概念?

包含一个或多个纯虚函数的类
不能实例化抽象类
但是可以定义抽象类的指针和引用

3、C++对抽象类具有以下限定

  • 抽象类中含有纯虚函数,由于纯虚函数没有实现代码,所以不能建立抽象类的对象。
  • 抽象类只能作为其他类的基类,可以通过抽象类对象的指针或引用访问到它的派生类对象,实现运行时的多态性。
  • 如果派生类只是简单地继承了抽象类的纯虚函数,而没有重新定义基类的纯虚函数,则派生类也是一个抽象类。

虚函数的用法

为什么要使用虚函数,因为我们希望基类指针如果指向派生类对象,调用的应该是派生类中的方法,如果不用虚函数,只用重载,那他将会调用基类的方法,所以需要虚函数

class A {
    virtual void info() { cout << "A: info" << endl;}
};

class B : public A {
public:
    void info() { cout << "B: info" << endl;}
};

int main(int argc, char const *argv[])
{
    A* b = new B(3, 4);
    b->info();
    return 0;
}
没有virtual时,调用基类方法:
A: info

加了virtual,调用派生类方法:
B: info

纯虚函数

抽象类中含有虚函数,继承类中需要实现抽象类的所有虚函数
成员函数可以调用虚函数,构造函数和析构函数不能调用虚函数

class A {
public:
    virtual void f() = 0;   //纯虚函数
    void g() {
        this->f();          // 调用虚函数
    }
};

class B : public A {
public:
    void f() {      // 派生类要实现虚函数
        cout << "B: f()" << endl;
    }
};

int main(int argc, char const *argv[])
{
    A a;            // error,不能创建抽象类对象
    A* a = new A;   // error,不能创建抽象类指针对象
    B* b = new B;
    b->g();         //因为B对A是公有继承,所以可以使用A中的成员方法
    return 0;
}

虚函数是用虚函数表实现的,虚函数表由一系列函数指针组成,每个函数指针指向虚函数的实现
从下面的例子可以看出,Base含有2个int和一个虚函数(几个都一样),Derived含有2个int,他们差了一个指针的大小(8)
class Base
{
    int x, y;
public:
    virtual void doSomeThing() = 0;
};

class Derived
{
    int x, y;
};

int main(int argc, char const *argv[])
{
    cout << sizeof(int) << endl;
    cout << sizeof(int*) << endl;
    cout << sizeof(Base) << endl;
    cout << sizeof(Derived) << endl;
    return 0;
}
4   // int占用4字节
8   // 指针占用8字节
16  // 2个int+1个虚函数指针
8   // 2个int

抽象类构造函数

构造函数不能是虚函数
可以用基类指针指向派生类对象
protected允许派生类和友元类访问,但禁止在继承结构层次外部访问

class A {
public:
    A(int i) : x(i) { cout << "A constructor" << endl; }
    virtual void f() = 0;
    void g() {
        this->f();
    }
protected:  // 成员变量声明为protected,可以在派生类和友元类中访问
    int x;
};

class B : public A {
public:
    B(int i, int j) : A(i), y(j) { cout << "B constructor" << endl; }
    void f() {
        cout << "B: f(" << this->x << ", " << this->y << ")" << endl;
    }
private:
    int y;
};

int main(int argc, char const *argv[])
{
    A* b = new B(3, 4); //基类指针指向派生类对象
    b->g();
    return 0;
}
A constructor
B constructor
B: f(3, 4)

可以看到先调用了A的构造函数,后调用了B的构造函数

虚析构函数

class A {
public:
    virtual ~A() { cout << "A destructor" << endl; }
};

class B : public A {
public:
    ~B() { cout << "B destructor" << endl; }
};

int main(int argc, char const *argv[])
{
    A* b = new B(3, 4);
    b->g();
    delete b;
    return 0;
}
析构函数没有加virtual时:
A constructor
B constructor
B: f(3, 4)
A destructor

析构函数加virtual时:
A constructor
B constructor
B: f(3, 4)
B destructor    // 先调用了派生类的析构函数
A destructor

用基类指针指向派生类对象时,如果基类的析构函数没有声明为virtual,销毁对象时就只会调用基类的析构函数,这样会造成内存泄漏
所以把基类的析构函数声明为virtual,销毁对象时就会先调用派生类的析构函数,再调用基类的析构函数

虚继承

为了解决菱形问题,一个基类有多个派生类,这些派生类又被一个类继承

class A {
public:
    int x;
    A() { cout << "A: constructor" << endl; }
};

class B : public A {};
class B1 : public A {};

class C : public B, public B1 {
public:
    C() { cout << "C: constructor" << endl; }
};

int main(int argc, char const *argv[])
{
    C c;
    c.x = 10;   // error,发生混淆,不知道是谁的x
    return 0;
}
A: constructor  // 调用了两次基类的构造函数
A: constructor
C: constructor

解决方法:基类的多个派生类使用虚继承,确保只有一个基类实例
class A {
public:
    int x;
    A() { cout << "A: constructor" << endl; }
};

class B : public virtual A {};
class B1 : public virtual A {};

class C : public B, public B1 {
public:
    C() { cout << "C: constructor" << endl; }
};

int main(int argc, char const *argv[])
{
    C c;
    c.x = 10;   // yes
    return 0;
}
A: constructor  // 只调用了一次基类的构造函数
C: constructor


文章作者: kunpeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kunpeng !
  目录