硬核两万字带你理解C++之多态

x33g5p2x  于2022-02-07 转载在 C/C++  
字(17.6k)|赞(0)|评价(0)|浏览(185)

多态

多态: 多态就是函数调用的多种形态,调用函数更加灵活,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

话不多说,我们首先来看这样的一段程序,让大家感知一下多态是什么意思,这是一段不构成多态的程序:

class Person
{
public:
    void BuyTicket()
    {
        cout<<"正常排队 - 全价买票"<<endl;
    }
protected:
    int _age;
    string _name;
}
class Student : public Person
{
public:
    void BuyTicket()
    {
        cout<<"正常排队 - 半价买票"<<endl;
    }
protected:
	//...
}
class Soldier : public Person
{
public:
    void BuyTicket()
    {
        cout<<"优先排队 - 全价买票"<<endl;
    }
protected:
	//...
}
void Func(Person* ptr)
{
    ptr->BuyTicket();
}
int main()
{
    Person ps;
    Student st;
    Soldier sd;
    
    Func(&ps);
    Func(&st);
    Func(&sd);
    
    return 0;
}

可以看到此时并没有构成多态,都打印的是父类的函数,因为形成多态具有两个条件

1、子类重写父类的虚函数

2、必须是父类的指针或者引用去调用虚函数

这两个条件什么意思,我们下面讲解,我们再来看看构成多态的程序:

class Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"正常排队 - 全价买票"<<endl;
    }
protected:
    int _age;
    string _name;
}
class Student : public Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"正常排队 - 半价买票"<<endl;
    }
protected:
	//...
}
class Soldier : public Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"优先排队 - 全价买票"<<endl;
    }
protected:
	//...
}
void Func(Person* ptr)
{
    //ptr指向父类对象调用父类的虚函数,指向子类对象调用子类虚函数
    ptr->BuyTicket();
}
int main()
{
    Person ps;
    Student st;
    Soldier sd;
    
    Func(&ps);
    Func(&st);
    Func(&sd);
    
    return 0;
}

此时满足了多态条件,看到了指针指向的对象是什么类就调用哪个类的虚函数:

那么什么是虚函数呢?多态的原理是什么呢?上面为什么就满足了多态的条件呢?等等问题我们下面开始深入理解多态:

有些书籍会把多态进行更细划分:

  • 静态的多态:函数重载,调用同一个函数,传不同的参数,就有不同的行为/形态
  • 动态的多态:父类指针或引用调用重写虚函数,不同的对象去调用,就有不同的行为/行为,父类指针或者引用指向父类,调用的就是父类的虚函数,父类指针或者引用指向哪个子类,调用的就是子类的虚函数
int main()
{
    int i;
    char ch;
    cin >> i;
    cin >> ch;
    cout << i << endl;
    cout << ch << endl;
    return 0;
}

这里其实就是静态的多态,看起来我们用的是一个函数,但是实际不是的,这个底层就是多态实现的:operator>>(int i); operator>>(char i);

int main()
{
    int i = 0,j = 1;
    double d = 1.1, e = 2.2;
    swap(i,j);
    swap(d,e);
    return 0;
}

多态就是函数调用的多种形态,以上都是静态的多态,这里的静态是指编译时确定的,在编译阶段通过函数修饰规则去找函数。动态的多态:不同的类型对象去完成同一件事情,产生的动作是不一样的

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。
Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件:

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

class Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"买票-全价"<<endl;
    }
};

注意:

  • 只有类的非静态成员函数才可以加virtual
  • 虚函数这里virtual和虚继承中用的virtual是同一个关键字,但是他们之间没有关系,这里的虚函数是为了实现多态,虚继承是为了解决菱形继承的数据冗余和二义性

构成多态的条件之一是虚函数重写:

虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。

class Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"买票-全价"<<endl;
    }
};
class Student : public Person
{
public:
    //子类的虚函数重写了父类的虚函数
    virtual void BuyTicket()
    {
        cout<<"买票-半价"<<endl;
    }
};
class Soldier : public Person
{
public:
    //子类的虚函数重写了父类的虚函数
    virtual void BuyTicket()
    {
        cout<<"买票-优先"<<endl;
    }
};
void f(Person& p)
{
    //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    p.BuyTicket();
}
void f(Person* p)
{
    //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    p->BuyTicket();
}
int main()
{
    Person p;//普通人
    Student st;//学生
    Soldier so;//军人
    
    f(p);
    f(st);
    f(so);
    
    f(&p);
    f(&st);
    f(&so);
    return 0;
}

注意多态需要满足的条件:

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

正常的虚函数重写,要求虚函数的函数名、参数、返回值都要相同,但是协变例外

虚函数重写的两个例外

  • 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A{};
class B : public A {};
class Person 
{
public:
	virtual A* f() 
    {
        cout<<"A* Person::f()"<<endl;
        return new A;
    }
};
class Student : public Person 
{
public:
	virtual B* f() 
    {
        cout<<"B* Student::f()"<<endl;
        return new B;
    }
};
int main()
{
    Person p;
    Student s;
    
    Person* ptr = &p;
    ptr->f();
    
    ptr = &s;
    ptr->f();
    return 0;
}

  • 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person
{
public:
	~Person() {cout << "~Person()" << endl;}
};
class Student : public Person 
{
public:
	~Student() { cout << "~Student()" << endl; }
};
int main()
{
    Person p
    Student s;
 
    return 0;
}

不构成重写:

先析构子类的对象,调用子类的析构函数,在子类的析构函数结束时自动的调用了父类的析构函数,最后调用父类的析构函数析构父类

构成重写:

在普通场景下,父子类的析构函数是否构成重写不重要,没什么影响,那么看下面的场景(new对象特殊场景):

class Person
{
public:
    //建议把父类的析构函数定义成虚函数
    //这样子类的虚函数方便重写父类的虚函数
	~Person() {cout << "~Person()" << endl;}
};
class Student : public Person 
{
public:
    //Student和Person析构函数名看起来不相同,但是他们构成虚函数重写
	~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
    Person* p1 = new Person;
    Person* p2 = new Student;
    delete p1;
    delete p2;
    return 0;
}

不构成重写:

为什么会这样呢?

delete p1;//p1->destuctor()+operator delete(p1)
delete p2;//p2->destuctor()+operator delete(p2)

因为delete时底层会去调用该对象类的析构函数和operator delete,不是虚函数时,他们构成隐藏,因为p1和p2都是父类指针,所以他们都是去调用父类的析构函数

构成重写时:

只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。

注意:

当子类的虚函数重写了父类的虚函数,子类虚函数不写virtual关键字也认为它是虚函数,完成了重写:

class Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"买票-全价"<<endl;
    }
};
class Student : public Person
{
public:
    //子类的虚函数重写了父类的虚函数
    void BuyTicket()
    {
        cout<<"买票-半价"<<endl;
    }
};
void f(Person& p)
{
    //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    p.BuyTicket();
}

int main()
{
    Person p;//普通人
    Student st;//学生
    
    f(p);
    f(st);
    return 0;
}

但是父类重写的函数不加virtual就不行,因为子类是先继承父类的虚函数,继承下来以后有virtual属性了,子类只是重写这个virtual函数,严格来说这个也算是C++语言设计的坑

针对虚函数重写,给一个建议:尽量不要写协变,尽量严格按重写要求来,这样代码更容易进行维护

C++11 override和final

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序
写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

如果不想虚函数被重写,那么就在虚函数后面加关键字final:

class Person
{
public:
    virtual void BuyTicket() final
    {
        cout<<"买票-全价"<<endl;
    }
};
class Student : public Person
{
public:
    //子类的虚函数重写了父类的虚函数
    void BuyTicket()
    {
        cout<<"买票-半价"<<endl;
    }
};
void f(Person& p)
{
    //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    p.BuyTicket();
}

int main()
{
    Person p;//普通人
    Student st;//学生
    
    f(p);
    f(st);
    return 0;
}

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Person
{
public:
    virtual void BuyTicket() 
    {
        cout << "买票-全价" << endl;
    }
};
class Student : public Person
{
public:

    void BuyTicket(int i) override
    {
        cout << "买票-半价" << endl;
    }
};
void f(Person& p)
{
    p.BuyTicket();
}

int main()
{
    Person p;//普通人
    Student st;//学生

    f(p);
    f(st);
    return 0;
}

如何设计出一个不能被继承的类?将该类的成员的访问限定符设置为私有:

class A
{
private:
    A()
    {}
}
class B:public A
{
public:
    B()
    {}
}
int main()
{
    B b;
    return 0;
}

这个是可以被继承,但是父类的成员子类不可见,子类定义对象不能使用父类的成员。

在C++11中支持了final这个关键字:

class A final
{};
class B:public A
{};
int main()
{
    return 0;
}

final修饰一个类,这个类不能被继承,不管定不定义对象直接报错。

重载、覆盖(重写)、隐藏的对比

总结

多态:调用一个函数时,展现出多种形态(通过调用不同的函数,完成不同的行为)。多态分为静态的多态和动态的多态:比如函数重载就是静态的多态,在编译时确定地址。动态的多态:条件:1、子类继承父类,完成虚函数的重写 2、父类的指针或者引用去调用这个重写的虚函数。

父类的指针或者引用指向父类对象,调用的是父类的虚函数

父类的指针或者引用指向子类对象,调用的是子类的虚函数

虚函数的重写条件:1、要是虚函数 2、函数名、参数、返回值都相同

例外:

1、协变(返回值不一样,父类的虚函数返回的是基类对象指针和引用,子类的虚函数返回的是子类对象指针和引用)

2、析构函数

3、子类中的重写的虚函数可以不加virtual关键字(建议加上)

抽象类

概念

在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口继承。

//抽象类
class Car
{
public:
    virtual void Drive() = 0;
};
int main()
{
    Car c;//不能实例化出对象
    return 0;
}

抽象类不能实例化出对象,可以更好的去表示现实世界中没有实例对象对应的抽象类型,比如:植物、人、动物,它体现了接口继承,强制子类去重写虚函数,如果不重写,继承下来还是纯虚函数,照样无法实例化出对象。

//抽象类
class Car
{
public:
    virtual void Drive() = 0;
};
class Benz : public Car
{
public:
    virtual void Drive()
    {
        cout<<"Benz-舒适"<<endl;
    }
};
int main()
{
    //Car c;//不能实例化出对象
    Car* pBenz = new Benz;
    pBenz->Drive();
    return 0;
}

要注意和override区分,override检查子类虚函数是否完成重写。纯虚函数是强制子类去重写虚函数,如果不重写,继承下来还是纯虚函数,照样无法实例化出对象。

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

多态的原理

虚函数表

这里常考一道笔试题:sizeof(Base)是多少?

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
private:
	int _b = 1;
    char _ch = 'a';
};
int main()
{
    cout << sizeof(Base) << endl;
    return 0;
}

为什么是12呢?根据内存对齐应该是8呀,这里为什么会是12。

是因为只要包含虚函数的类,该类的对象就包含一个虚函数表指针(简称虚表指针),这个虚函数表指针就是用来实现多态的:

这个虚表指针指向一个数组,这个数组的元素是函数指针,这里面的函数指针指向该类中的虚函数

虚函数被编译成指令后,还是和普通函数一样,存在代码段,只是它的地址放在虚表中

需要注意的是:
里跟虚继承那里是不一样的,他们虽然都用了virtual关键字,但是他们的使用场景完全不一样,解决的也是不一样的问题,他们之间没有关联,虚继承产生的是虚基表,虚基表里面存的是距离虚基类的偏移量

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
     virtual void Func1()
     {
         cout<<"Person::Func1()"<<endl;
     }
};
class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
    //不重写Func1,虚表里面的指针指向的是父类的虚函数
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
    Person Mike;
    Func(Mike);
    Student Johnson;
    Func(Johnson);
    return 0;
}

父子类无论是否完成虚函数的重写,都有各自的独立的虚表,一个类的所有对象共享一个虚表

满足多态条件以后,构成多态:指针或者引用在调用虚函数时,不是在编译时确定,是在运行时到指针或引用指向的对象的虚表中去找对应的虚函数调用,如果指向的是父类对象,则调用的就是父类的虚函数,指向的是子类的对象,调用的就是子类的虚函数。需要注意的是,如果不构成多态,那么这里调用的时候就是编译时确定的调用哪个函数,主要看的p的类型,调用的就是Person的Buyticket,跟传什么类型对象过来没有关系。

总结
构成多态,指向谁,调用谁的虚函数,跟对象有关;不构成多态,对象类型是什么,调用的就是哪个对象的函数,跟类型有关

为什么多态的条件之一必须是父类的指针或者引用去调用虚函数时发生多态,父类对象确不行呢?

父类的指针和引用,在切片时,指向或者引用父类对象 或者 指向或者引用子类对象中切出来的父类的那一部分。vfptr在对象的前四个字节保存,指向父类看到的是父类的虚表,指向子类看到的是子类的虚表

如果为父类对象时,切片只会拷贝成员变量过去,不会拷贝vfptr过去,因为拷贝过去不合理,如果可以拷贝过去,因为一个类共享一个虚表,再创建一个父类对象,这个父类对象的虚表是子类的虚表,这样明显是不合理的

多态实际上有一些性能开销的:

我们通过汇编代码分析,可以看出满足多态的函数调用不是在编译时确定的,是运行起来以后到对象中去找的,不满足多态的函数调用是编译或者链接时确认好的:

满足多态时调用的虚函数汇编代码

call eax其实就是调用虚函数

不满足多态时调用的虚函数汇编代码

动态绑定和静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如函数重载
  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
    普通函数的调用,编译(当在一个文件当中时在编译阶段确定)链接(当声明和定义分离时,在链接时确定)时确定地址,多态的调用是运行时确定地址,如何确定?去指向对象的虚函数表中找到虚函数的地址

下面我们来看这样的几个问题

  • 对象中虚表指针是在什么阶段初始化的呢?虚表又是在什么阶段生成的呢?
    对象中虚表指针是在构造函数初始化列表进行初始化,虚表是在编译时就生成好了

可以看到调用了构造函数后虚表指针依旧进行初始化了

  • 虚函数放在虚表里面的,这句话对吗?
    这句话不准确,虚表里面放的是虚函数地址,虚函数跟普通函数一样,编译完成后,都是放在代码段。

  • 一个类中所有的虚函数地址,都会放在虚表中。
    这句话是正确的,虽然可能大家有时候在调式的监视窗口看不到某个虚函数,这是因为编译器进行了优化,其实在内存中是可以看到有的,这些虚函数的地址都会放在虚表当中。

  • 虚函数的重写,也叫做虚函数的覆盖,原因是子类刚开始是拷贝父类的虚函数过来,如果重写了哪个虚函数,就会将该虚函数拿过来进行覆盖从父类拷贝过来的虚函数

  • vs下会在虚表结束位置放一个空指针表示虚表结束了

在面试中,面试官可能会问虚表是存在哪里的?想办法写一段程序,论证一下虚表存在哪个区域的?那么怎么论证呢?我们定义各个区域的变量或者常量,通过看地址的方式看哪个地址和虚表指针的内容相近:

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
	p.BuyTicket();
}
int j = 0;
int main()
{
    //取虚表地址打印一下
    Person p;
    Person* pp = &p;//pp指向整个对象p
    printf("vftptr:%p\n",*((int*)pp));//将pp强转为int*,即pp指向对象p的前四个字节,对它解引用就拿到了前四个字节,前四个字节就是vftptr(虚表指针)
    
    int i;
    printf("栈上地址:%p\n",&i);
    printf("数据段地址:%p\n",&j);
    
    int *k = new int;
    printf("堆地址:%p\n",k);
    char* cp = "hello world";
    printf("代码段地址:%p\n",cp);
    return 0;
}
printf("vftptr:%p\n",*((int*)pp));

这个代码就打印出来了虚表指针,为什么呢?pp指向整个对象p,首先将pp强转为int,此时pp指向p对象的前四个字节,对它解引用就拿到了前四个字节,这前四个字节就是虚表指针。*

可以看到它是更接近代码段地址的,所以虚表是存在代码段的。虚函数编译出来函数指令跟普通函数一样,存在代码段,虚函数地址又被放到虚函数表中

单继承和多继承关系的虚函数表

单继承的虚函数表

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

我们通过调试发现监视窗口看不到子类自己的虚函数func3和func4

所以监视窗口不一定真实,但是内存上实际上是有的:

我们可以通过写一个程序打印一下虚表,通过调用虚表中的虚函数,确定我们上面说的那两个就是func3和func4的地址:

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};
typedef void(*VFunc)();//VFunc是函数指针
//void PrintVFT(VFunc ptr[])
void PrintVFT(VFunc ptr[])//存函数指针的数组指针,ptr指向函数指针数组
{
    for(int i = 0;ptr[i]!=nullptr;++i)
    {
        printf("VFT[%d]:%p\n",i,ptr[i]);
        ptr[i]();//调用该函数,确认地址是哪个函数的地址
    }
    printf("\n");
}
int main()
{
	Base b;
    PrintVFT((VFunc*)(*(int*)&b));//将b的虚表指针传过去,*(int*)&b)拿到虚表指针
	Derive d;
    PrintVFT((VFunc*)(*(int*)&d));//将d的虚表指针传过去
    
	return 0;
}

可以看到的确成功调用了func3和func4

多继承的虚函数表

我们来看一个多继承的虚函数表是怎么样的:

class Base1{
public:
    virtual void func1() {cout << "Base1::func1" << endl;}
    virtual void func2() {cout << "Base1::func2" << endl;}
private:
	int b1;
};
class Base2 {
public:
    virtual void func1() {cout << "Base2::func1" << endl;}
    virtual void func2() {cout << "Base2::func2" << endl;}
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
    virtual void func1() {cout << "Derive::func1" << endl;}
    virtual void func3() {cout << "Derive::func3" << endl;}
private:
	int d1;
};
typedef void(*VFunc)();//VFunc是函数指针
//void PrintVFT(VFunc ptr[])
void PrintVFT(VFunc* ptr)//存函数指针的数组指针,ptr指向函数指针数组
{
    for(int i = 0;ptr[i]!=nullptr;++i)
    {
        printf("VFT[%d]:%p\n",i,ptr[i]);
        ptr[i]();//调用该函数,确认地址是哪个函数的地址
    }
    printf("\n");
}
int main()
{
    Base1 b1;
    Base2 b2;
    
    Derive d;
    PrintVFT((VFunc*)(*(int*)&d));//打印第一个虚表(d对象中起始位置为虚表指针)
    PrintVFT((VFunc*)(*(int*)((char*)&d+sizeof(Base1))));//打印第二个虚表
    
    return 0;
}

可以看到多继承中,Derive既继承了Base1又继承了Base2,Derive就有两张虚表:

我们看到Derive中自己的虚函数func3在监视窗口并没有,那么怎么证明它是存在的呢?和上面其实是一样的,只不过打印第二张虚表有些不一样:

typedef void(*VFunc)();//VFunc是函数指针
//void PrintVFT(VFunc ptr[])
void PrintVFT(VFunc ptr[])//存函数指针的数组指针,ptr指向函数指针数组
{
    for(int i = 0;ptr[i]!=nullptr;++i)
    {
        printf("VFT[%d]:%p\n",i,ptr[i]);
        ptr[i]();//调用该函数,确认地址是哪个函数的地址
    }
    printf("\n");
}
int main()
{
    Base1 b1;
    Base2 b2;
    
    Derive d;
    PrintVFT((VFunc*)(*(int*)&d));//打印第一个虚表(d对象中起始位置为虚表指针),需要强转为VFunc*,因为虚表指针指向的类型是函数指针数组
    PrintVFT((VFunc*)(*(int*)((char*)&d+sizeof(Base1))));//打印第二个虚表
    
    return 0;
}

打印第一张虚表:

PrintVFT((VFunc*)(*(int*)&d));

因为d对象中起始位置为第一张虚表的虚表指针,需要强转为VFunc*,因为虚表指针指向的类型是函数指针数组,这个和前面验证单继承没有区别,但是打印第二张虚表就有些不一样了:

PrintVFT((VFunc*)*((int*)((char*)&d+sizeof(Base1))));

首先将取地址d将他转为char类型,加上Base1的大小就到了Base2,Base2的前四个字节是虚表指针,所以再强转为int,然后解引用拿到这四个字节,最后强转为VFunc*

这样就可以打印这两张虚表了:

可以看到func3是存在第一张虚表当中的。

继承和多态常见的面试问题

  1. 下面哪种面向对象的方法可以让你变得富有( )
    A: 继承 B: 封装 C: 多态 D: 抽象
  2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
    A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
  3. 面向对象设计中的继承和组合,下面说法错误的是?()
    A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
    B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
    C:优先使用继承,而不是组合,是面向对象设计的第二原则
    D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
  4. 以下关于纯虚函数的说法,正确的是( )
    A:声明纯虚函数的类不能实例化对象

B:声明纯虚函数的类是虚基类

C:子类必须实现基类的纯虚函数

D:纯虚函数必须是空函数

  1. 关于虚函数的描述正确的是( )
    A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型

B:内联函数不能是虚函数

C:派生类必须重新定义基类的虚函数

D:虚函数可以是一个static型的函数
内联函数是没有地址的,在调用的地方展开,虚函数要把地址放进虚函数表中

  1. 关于虚表说法正确的是( )
    A:一个类只能有一张虚表
    B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
    C:虚表是在运行期间动态生成的
    D:一个类的不同对象共享该类的虚表
  2. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
    A: A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
    B: A类对象和B类对象前4个字节存储的都是虚基表的地址
    C: A类对象和B类对象前4个字节存储的虚表地址相同
    D: A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
  3. 下面程序输出结果是什么? ()
#include<iostream>
using namespace std;
class A{
public:
  A(char *s) 
  { 
      cout<<s<<endl; 
  }
  ~A(){}
};
class B:virtual public A
{
public:
  B(char *s1,char*s2):A(s1) 
  { 
      cout<<s2<<endl; 
  }
};
class C:virtual public A
{
public:
  C(char *s1,char*s2):A(s1) 
  { 
      cout<<s2<<endl; 
  }
};
class D:public B,public C
{
public:
  D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
  { 
      cout<<s4<<endl;
  }
};
int main() 
{
  D *p=new D("class A","class B","class C","class D");
  delete p;
  return 0;
}

A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D

首先调用D的构造函数初始化,然后D的构造函数需要先调用父类的构造函数初始化父类的部分,需要按继承的声明顺序进行初始化,所以先要初始化B,调用B的构造函数,B又是继承A,调用B的构造函数时先调用A的构造函数进行初始化,所以先打印A再打印B,然后再初始化C,调用C的构造函数,这时候打印C,因为是虚拟继承,此时C类不会再去调用A类的构造函数,此时打印C,然后本来要掉A类的构造函数,但是是虚拟继承,之前调用过了就不进行调用了,然后再初始化D类的部分,然后打印了D,故最后的结果为选项A

  1. 多继承中指针偏移问题?下面说法正确的是( )
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;
    return 0;
}

A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

这是一个切片问题,将子类对象的地址赋给父类的指针,父类的指针指向子类对象中自己的那一部分,Base1在继承的声明顺序在前,所以Base1的成员在子类的最前面,所以p1指向d对象的最前面,p3就是子类的指针,所以p3也指向d对象的最前面,但是p2是Base2类的指针,p2指向中间自己的部分,所以p1==p3!=p2。

  1. 以下程序输出结果是什么()
class A
{
public:
    virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
    virtual void test(){ func();}
                     //A* this
};
class B : public A
{
public:
	void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
    B*p = new B;
    p->test();//p->test(p),将p传给this,A* this = p;this指向子类中父类的那一部分,this->func(),this此时是指向子类对象,去子类的虚表中去找func函数,调用子类的func函数,普通函数是实现继承,虚函数的继承是接口继承,将接口继承下来,所以子类中的缺省值是没有用的,所以最后打印的是B->1
    return 0;
}

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

B类继承A类,子类重写了虚函数func,test没重写,在子类的虚函数表中的test函数是拷贝父类的,子类指针p调用test时,其实这里隐含的有this指针,test函数里面有隐含的this指针,A*this,p->test§,将p传给this,父类的指针指向了子类对象,this->func(),这里发生了多态,去调用了子类的func,所以可以将A,C排除,又因为普通函数是实现继承,虚函数的继承是接口继承,将接口继承下来,所以子类中的缺省值是没有用的,所以最后打印的是B->1

多态问答题

  1. 什么是多态?
    多态:调用一个函数时,展现出多种形态(通过调用不同的函数,完成不同的行为)。多态分为静态的多态和动态的多态:比如函数重载就是静态的多态,在编译时确定地址。

动态的多态:条件:1、子类继承父类,完成虚函数的重写 2、父类的指针或者引用去调用这个重写的虚函数。父类的指针或者引用指向父类对象,调用的是父类的虚函数

父类的指针或者引用指向子类对象,调用的是子类的虚函数

  1. 什么是重载、重写(覆盖)、重定义(隐藏)?

  1. 多态的实现原理?
    用父类的指针或者引用去调用虚函数,父类的指针指向父类对象,在运行时到父类的虚函数表中去找函数,父类的指针指向子类对象,在运行时到子类的虚函数表中去找函数,怎么去找呢?在一个类中,只要有虚函数就会有一个虚表指针,指向虚函数表,虚表指针在构造函数的初始化列表进行初始化,虚函数表是在编译时生成的,普通函数的调用,编译(当在一个文件当中时在编译阶段确定)或者(当声明和定义分离时)链接时确定地址,多态的调用是运行时确定地址,如何确定?去指向对象的虚函数表中找到虚函数的地址

  2. inline函数可以是虚函数吗?
    可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。VS当中是这样的,准确来说内联不能是虚函数,内联函数是没有地址的,在调用的地方展开,虚函数要把地址放进虚函数表中

  3. 静态成员可以是虚函数吗?
    虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable。 对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual

  4. 构造函数可以是虚函数吗?
    1、构造函数搞成虚函数没有意义,因为子类中要调用父类构造函数初始化。写成虚函数目的是多态,构造函数不需要多态的方式。2、对象的虚函数表指针是在构造函数初始化列表阶段初始化的,如果构造函数是虚函数,那么调用构造函数时对象中的虚表指针都没有初始化。

  5. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
    可以,并且最好把基类的析构函数定义成虚函数。在new对象时,delete对应对象要发生多态,析构函数要是虚函数

  6. 对象访问普通函数快还是虚函数更快?
    首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

  7. 虚函数表是在什么阶段生成的,存在哪的?
    虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

  8. C菱形继承的问题?虚继承的原理?
    **C
    中菱形继承的主要问题是数据冗余和二义性,虚继承是为了解决这两个问题提出的,虚继承原理是,菱形虚拟继承中在腰部的两个类的对象模型内存当中多了两个指针,这两个指针指向了虚基表,这里面存了距离数据冗余的那个成员的偏移量**

  9. 什么是抽象类?抽象类的作用?
    在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口继承。

相关文章