C++ 多态:重塑编程效率与灵活性

news/2024/10/5 1:50:13 标签: c++, 开发语言, c语言

目录

多态的概念

多态的定义及实现

多态的构成条件

虚函数

虚函数的重写

虚函数重写的两个例外:

1. 协变(基类与派生类虚函数返回值类型不同)

2. 析构函数的重写(基类与派生类析构函数的名字不同)

析构函数要不要定义成虚函数???

C++11 override 和 final

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

抽象类

概念

接口继承和实现继承

多态的原理

虚函数表

多态的原理

如何实现指向谁调用谁的虚函数???

虚表的进一步理解

1.虚函数的地址存在哪里,进程地址空间这个角度来看

2.来观察如下的代码,再次来理解重写(覆盖)

3.虚函数表(虚表)存在哪里???

如何证明虚表存在代码段???

动态绑定与静态绑定

静态绑定

动态绑定

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

单继承中的虚函数表

​编辑如何打印出真实的虚表

打印虚函数表

多继承中的虚函数表

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

一、选择题

二、问答题

1. 什么是多态?

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

3. 多态的实现原理?

4. inline函数可以是虚函数吗?

5. 静态成员可以是虚函数吗?

6. 构造函数可以是虚函数吗?

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

8. 对象访问普通函数快还是虚函数更快?

9. 虚函数表是在什么阶段生成的,存在哪的?

10. C++菱形继承的问题?虚继承的原理?

11. 什么是抽象类?抽象类的作用?


多态的概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

举个例子

超市购物优惠

 

在超市中,不同类型的顾客在购物时会有不同的优惠行为。普通顾客按照商品原价进行购买;会员顾客可以享受积分和一定比例的折扣;老年人顾客在特定的日子里可以享受额外的优惠。

 

例如,同样是购买一件价值 100 元的商品,普通顾客需要支付 100 元;会员顾客可能因为有积分和折扣,只需支付 90 元;而老年人顾客在周三的敬老日购买这件商品,可能只需支付 85 元。

 

虽然都是进行购物这个行为,但不同类型的顾客有不同的支付方式和优惠力度,这体现了多态性。


多态的定义及实现

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 MemberShopper 继承了 NormalShopper。普通顾客购物全价,会员用户购物享受额外优惠。

要构成多态还有两个条件

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

//普通顾客购物
class NormalShopper 
{
public:
    virtual void BuyGoods() 
    {
        cout << "普通顾客:购物全价" << endl;
    }
};
//会员顾客购物
class MemberShopper : public NormalShopper {
public:
    virtual void BuyGoods() 
    {
        cout << "会员顾客:享受额外优惠购物" << endl;
    }
};

void ShoppingFunc(NormalShopper& shopper)  //父类的指针或者是引用调用虚函数
{
    shopper.BuyGoods();
}

int main() {
    NormalShopper normal;
    ShoppingFunc(normal);

    MemberShopper member;
    ShoppingFunc(member);
    return 0;
}

满足多态: 跟调用对象的类型无关,跟指向对象有关,指向哪个对象调用就是他的虚函数。

不满足多态: 跟调用对象的类型有关,类型是什么调用的就是谁的虚函数。

不满足多态,随便撤掉两个条件中的任意一个。

在ShoppingFunc函数的参数中,如果是 MemberShopper* p,这样不能把父类对象传给子类类型,当使用MemberShopper* p这种子类指针类型时,不能直接用父类对象来初始化它,因为子类指针期望指向的是子类类型的对象,而父类对象并不具备子类特有的成员和行为,所以这种赋值是不合法的。,但是子类可以传给父类,会发生切片,所以这就是为什么这里是父类的指针或者引用,而不是子类。

虚函数

虚函数:被 virtual 关键字修饰的成员函数被称为虚函数,可以修饰成员函数。

virtual关键字:

   1. 可以修饰成员函数,为了完成虚函数的重写,满足多态的条件之一。

   2. 可以在菱形继承中,去完成虚继承,解决数据冗余和二义性。

两个地方使用了同一个关键字,但是它们相互之间没有一点关联。

class NormalShopper 
{
public:
    virtual void BuyGoods()  //虚函数
    {
        cout << "普通顾客:购物全价" << endl;
    }
};

虚函数的重写

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

//普通顾客购物
class NormalShopper 
{
public:
    virtual void BuyGoods() 
    {
        cout << "普通顾客:购物全价" << endl;
    }
};
//会员顾客购物
class MemberShopper : public NormalShopper {
public:
    virtual void BuyGoods() 
    {
        cout << "会员顾客:享受额外优惠购物" << endl;
    }
    /*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写
    (因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,
    不建议这样使用*/
   /* void BuyGoods() {cout << "会员顾客:享受额外优惠购物" << endl;}*/
};

void ShoppingFunc(NormalShopper& shopper)  //父类的指针或者是引用调用虚函数
{
    shopper.BuyGoods();
}

int main() {
    NormalShopper normal;
    ShoppingFunc(normal);

    MemberShopper member;
    ShoppingFunc(member);
    return 0;
}

注意点:如果父类不是虚函数,不行。 子类不是虚函数可以 (去掉virtual),不建议这样使用。

原因是继承下来,父类是虚函数,子类勉强认可,如果继承下来父类都不是虚函数,子类更不是虚函数。

虚函数重写的两个例外:

1. 协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)

返回值可以不同,返回值必须是父子关系的指针或者引用

只要是父子关系的继承也可以

2. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。

析构函数要不要定义成虚函数???

①先看普通情况下,这种情况下加不加virtual都不影响,如果涉及到通过基类指针或引用指向派生类对象,并且在析构时希望正确地调用派生类的析构函数,那么就必须在基类的析构函数前加上virtual关键字。

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	Person p;
	Student s;
	return 0;
}

②一种特殊情况下,基类指针或引用指向派生类对象。

  • 先看不是虚函数的情况下
class Person
{
public:
	~Person()  //析构函数的函数名会被处理成destructor
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	~Student() //析构函数的函数名会被处理成destructor
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	Person* p1 = new Person;
	delete p1;

	Person* p2 = new Student;
	delete p2;
	return 0;
}

如果 Student 析构函数中有资源释放,这里没有被调用到,就会发生内存泄漏。

普通调用与类型有关,p1 p2的类型是 Person,所以只调用了 Person 的析构函数,所以结果只是析构了两次父类,没有去完成子类的资源清理。

我期望指向谁调用谁,就需要满足多态的两个条件:

① 父类的指针或者引用,已满足。

② 虚函数的重写(编译器已经处理成了destructor函数名相同,加上virtual就可以,其实写父类就可以,但是建议两个都加virtual)。

  • 定义成虚函数的情况下
class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	Person* p1 = new Person;
	delete p1;

	Person* p2 = new Student;
	delete p2;
	return 0;
}

当使用 delete p1 时,由于 p1 指向一个 Person 对象,所以会调用 Person 的析构函数,输出 ~Person()

 

当使用 delete p2 时,因为 p2 实际上指向一个 Student 对象,并且 Person 的析构函数被声明为虚函数,所以会先调用 Student 的析构函数输出 ~Student(),然后再调用 Person 的析构函数输出 ~Person()

所以析构函数是需要定义成虚函数的。

C++11 override 和 final

final关键字:

当用 final 修饰虚函数时,表示该虚函数不能在派生类中被重写。

virtual void func(int val = 1) final { }

当用 final 修饰类时,表示该类不能被继承。

class A final { } 表示类不能被继承。C++98做法把类的构造函数设置成私有表示该类不能被继承。

2.override关键字:

一种情况下,你想着重新,但是函数名写错了,编译也能通过。如果最后查找问题的时候,很不容易发现。所以就可以使用override进行检查,是否进行了重写,如果没有重写,编译报错。

class A
{
public:
	virtual void Drive() {}
};
class B : public A
{
public:
	virtual void Drive() override {}
};
int main()
{
	return 0;
}

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

重载重载的两个函数作用域在同一个,函数名相同,参数不同(参数的类型不同,参数的顺序不同,参数的个数不同,返回值没有要求)。

重定义(隐藏):两个函数作用域分别在基类和派生类,函数名相同,两个基类和派生类不构成重写就是重定义。

重写(覆盖):两个函数作用域分别在基类和派生类,函数名、返回值、参数必须相同(协变例外),两个函数必须是虚函数。


抽象类

概念

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

特点:不能实例化出对象。

作用

1. 强制子类必须重写,不重写自己用不了,实例化不出对象。

2. 表示抽象的类型,抽象就是在现实中没有对应的实体。

//抽象类
class Car
{
	virtual void Drive() = 0; //该函数不需要实现,叫做纯虚函数
};
//Beiz 还是一个抽象类,继承下来的纯虚函数,
// 一种办法,重写这个函数(不再是纯虚函数),就可以实例化出对象了
class Beiz : public Car
{
public:
	virtual void Drive() 
	{
		cout << "我不在是纯虚函数" << endl;
	}
};
int main()
{
	//Car car;
	Beiz bz;
	return 0;
}

接口继承和实现继承

接口继承

在接口继承中,主要是继承了函数的声明,子类必须自己提供函数的具体实现。这就像只继承了一个契约,承诺要实现某些特定的行为,但具体如何实现由子类决定。例如,当一个类继承了一个包含纯虚函数的基类时,就必须实现这些纯虚函数,否则该子类也会成为抽象类。

实现继承

实现继承确实是将声明和定义都继承了下来。子类继承了父类的成员函数和数据成员的声明以及它们的具体实现。子类可以直接使用父类中已有的实现,也可以根据需要重写某些函数以实现特定的行为变化。但即使不重写,也继承了父类的具体实现,可以直接调用这些函数。

综上所述:

虚函数就是接口继承,最终目的是实现这个接口,没有继承实现。

普通函数就是复用,实现继承,就是用它的实现


多态的原理

虚函数表

在以下代码中,sizeof(Base)是多少???

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};
int main()
{
	Base b;
	cout << sizeof(Base) << endl; 
	return 0;
}

通过观察测试我们发现在32位机器下 b 对象是 8 bytes。在64位机器下是16 bytes,因为指针占8字节,加上int的大小,再内存对齐,得到16。

除了_b成员,还多一个 _vftptr 放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function,t 代表table)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析 

多态的原理

当子类继承自含有虚函数的父类时,子类对象会继承父类的虚函数表指针,编译器无需为子类再单独创建一个虚函数表指针。

//普通顾客购物
class NormalShopper 
{
public:
    virtual void BuyGoods(){ cout << "普通顾客:购物全价" << endl; }
    int _p = 1;
};
//会员顾客购物
class MemberShopper : public NormalShopper {
public:
    virtual void  BuyGoods()
    { cout << "会员顾客:享受额外优惠购物" << endl; }
    int _s = 2;
};

void ShoppingFunc(NormalShopper& shopper)  //父类的指针或者是引用调用虚函数
{
    shopper.BuyGoods(); 
}

int main() {
    NormalShopper normal;
    ShoppingFunc(normal);

    MemberShopper member;
    ShoppingFunc(member);
    return 0;
}

如何实现指向谁调用谁的虚函数???

多态是在运行时到指向的对象的虚表中查找要调用的虚函数的地址来进行调用的。

在这个过程中,具体使用哪个虚函数表是由编译器在运行时根据对象的实际类型自动推导确定的。

当通过基类的指针或引用调用虚函数时,编译器会根据指针或引用实际所指向的对象的类型来确定要查找哪个对象的虚函数表。如果指向的是基类对象,就使用基类的虚函数表;如果指向的是派生类对象,就使用派生类的虚函数表,从而实现多态行为。

注意点:

当子类重写父类的虚函数后,从概念上看好像是虚表中相应的地址被覆盖了,但实际上并不是真正意义上的完全覆盖原地址。

 

在内存中,子类会创建自己的虚函数表,这个虚函数表首先包含从父类继承而来的未被重写的虚函数地址,接着在被重写的虚函数对应的位置放置子类重写后的虚函数地址。可以认为是在新的虚函数表中更新了特定位置的内容,但原父类的虚函数表依然存在于内存中的某个位置,只是在通过子类对象调用虚函数时,使用的是子类的虚函数表。

 

所以,不是严格意义上的 “覆盖” 原地址,而是创建了新的表并更新了特定位置的内容以适应子类的行为。

再来查看汇编代码来理解(了解)

 如果没有完成重写,是不是就不会覆盖了,假设注释掉子类虚函数

虚表的进一步理解

1.虚函数的地址存在哪里,进程地址空间这个角度来看

普通函数在编译后其指令存放在代码段中。当程序调用普通函数时,直接跳转到该函数在代码段中的特定位置执行。

 

对于虚函数,在编译时也会生成相应的指令并放在代码段中。而包含虚函数的类会有一个虚函数表,虚函数表中存储的是各个虚函数在代码段中的起始地址。当通过指向对象的指针或引用调用虚函数时,程序会根据对象的虚函数表指针找到虚函数表,再从虚函数表中获取特定虚函数的地址,然后跳转到该地址对应的代码段位置执行虚函数的指令。

虚表可能是栈区也可能是堆区

如果对象是在栈上创建的,那么虚函数指针就在栈上分配的对象内存空间中。
如果对象是在堆上分配的,虚函数指针就在堆内存中的对象空间里。

虚表结尾后面会加一个nullptr指针作为结束标志

这样做的目的主要是为了在遍历虚函数表时能够确定表的结束位置。当程序通过虚函数表指针查找虚函数地址时,可以依次检查表中的指针,直到遇到 nullptr 指针为止,从而知道已经到达了虚函数表的末尾。

2.来观察如下的代码,再次来理解重写(覆盖)

以下代码中,只对 Func1 进行了重写,其它父类的虚函数会继承下来

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Deriver : public Base
{
public:
	virtual void Func1()
	{
		cout << "Deriver::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Deriver d;
	return 0;
}

3.虚函数表(虚表)存在哪里???

代码段(常量区)

如何证明虚表存在代码段???

以下代码及监视窗口说明:

说明同类型的对象共用一个虚表,公共区域就不能是栈区,栈出了作用域就销毁。

公共区域有堆,堆是动态开辟的,数据段,通常存一些全局数据和静态数据,

而代码段通常存的是常量数据(只读)const值开始就给了初值,后面就不能修改了。

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Deriver : public Base
{
public:
	virtual void Func1()
	{
		cout << "Deriver::Func1()" << endl;
	}
private:
	int _d = 2;
};
void test1()
{
	Base b1;
	Deriver d1;
}
void test2()
{
	Base b2;
	Deriver d2;
}
int main()
{
	test1();
	test2();
	return 0;
}

虚函数表指针通常存在于对象所在的内存空间,而对象可能在栈上创建(此时虚函数表指针在栈区),也可能在堆上分配(此时虚函数表指针在堆区)。

这个虚函数表指针指向的虚函数表通常存放在代码段。当程序需要调用虚函数时,会通过对象的虚函数表指针找到对应的虚函数表,然后从虚函数表中获取特定虚函数的地址,进而跳转到代码段中该虚函数的指令处执行。

还可以写程序进行验证,虚函数表存在代码段

printf("vftptr:%p\n", *(int*)&b1);  就是类似于取数组的地址再进行解引用拿到数组中的地址。

强制转成 int* , 目的是为了便于观察虚函数表指针的地址。

  • Base b1;声明了一个Base类的对象b1。如果Base类中有虚函数,那么这个对象中通常会有一个指向虚函数表的指针(即虚函数表指针)。

  • *(int*)&b1

    • &b1取对象b1的地址。
    • (int*)&b1将这个地址强制转换为一个整数指针类型。这是因为在某些系统中,虚函数表指针的大小与整数的大小相同,这样做是为了能够以整数的方式访问对象起始位置的内容(假设虚函数表指针在对象的起始位置)。
    • *(int*)&b1则是通过这个整数指针访问所指向的内容,也就是获取对象b1中存储的虚函数表指针的值。
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Deriver : public Base
{
public:
	virtual void Func1()
	{
		cout << "Deriver::Func1()" << endl;
	}
private:
	int _d = 2;
};
void test1()
{
	Base b1;
	Deriver d1;
}
void test2()
{
	Base b2;
	Deriver d2;
}
//对比地址区别
void func()
{
	Base b1;
	printf("vftptr:%p\n", *(int*)&b1);

	int i = 0;
	int* p1 = &i;  
	int* p2 = new int;
	const char* p3 = "hello";
	printf("栈变量:%p\n", p1);
	printf("堆变量:%p\n", p2);
	printf("代码段常量:%p\n", p3);
	printf("代码段函数地址:%p\n", &Base::Func3); //类域下需要加上取地址符号
	printf("代码段函数地址:%p\n", func); //普通函数函数名就是地址
}
int main()
{
	/*test1();
	test2();*/
	func();
	return 0;
}

我们观察地址可以发现,在代码段的地址都是非常接近的。

动态绑定与静态绑定

静态绑定

  • 定义

    • 静态绑定也称为早绑定,是在编译阶段就确定了要调用的函数版本。
    • 这意味着编译器在编译程序时,根据对象的声明类型来决定调用哪个函数。
  • 特点

    • 效率高:由于在编译时就确定了函数调用,不需要在运行时进行额外的查找,所以执行速度相对较快。
    • 灵活性低:一旦编译完成,函数调用就不能根据对象的实际类型进行改变。
class Base {
public:
    void print() {
        std::cout << "Base class" << std::endl;
    }
};

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

int main() {
    Base b;
    b.print(); // 静态绑定,调用 Base 类的 print 函数
    return 0;
}

动态绑定

  • 定义

    • 动态绑定也称为晚绑定,是在程序运行时根据对象的实际类型来确定要调用的函数版本。
    • 这通常是通过虚函数和多态性来实现的。
  • 特点

    • 灵活性高:可以根据对象的实际类型在运行时选择合适的函数实现,提供了更大的灵活性。
    • 性能开销:由于需要在运行时进行查找,动态绑定通常比静态绑定稍微慢一些。
class Base {
public:
    virtual void print() {
        cout << "Base class" << endl;
    }
};

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

int main() {
    Base* b = new Derived();
    b->print(); // 动态绑定,调用 Derived 类的 print 函数
    delete b;
    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 Deriver : 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;
	Deriver d;
	return 0;
}

如何打印出真实的虚表

在打印之前,我们先来复习一下函数指针的声明以及定义

函数指针的声明形式为:返回类型 (* 指针变量名)(参数列表);

例如,声明一个指向返回值为int,有两个int类型参数的函数指针:

int (*funcPtr)(int, int);

函数指针的定义:

可以通过将一个具有相同返回类型和参数列表的函数名赋给函数指针来定义它。

例如,如果有一个函数int add(int a, int b),可以这样定义函数指针:

   int add(int a, int b) {
       return a + b;
   }

   int main() {
       int (*funcPtr)(int, int) = add;
       int result = funcPtr(3, 4);
       cout << "Result: " << result << endl;
       return 0;
   }

注意:当把一个函数名赋给函数指针时,不需要取地址符。这是因为在大多数情况下,函数名会被隐式地转换为指向该函数的指针。

打印虚函数表
class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base::func2()" << endl;
	}
private:
	int a;
};
class Deriver : 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(*VF_PTR)(); //函数指针类型重定义名称
void PrintVFTable(VF_PTR* pTable)  //假设里面存的是 int 就是 int* pTable;
                                  //里面存的是函数指针就是 VF_PTR* pTable;
{
	//虚函数表结束标志是 nullptr 
	for (size_t i = 0; pTable[i] != 0; ++i)
	{
		printf("vfTable[%d]:%p->", i, pTable[i]);
		VF_PTR func = pTable[i]; //虚函数地址赋值给 func 类型是 VF_PTR 函数指针类型
		func();  //调用虚函数
	}
	cout << endl;
}
int main()
{
	//函数指针的定义,和普通的指针不一样
	//void(*p)();
	Base b;
	Deriver d;
	//取对象中前四个字节存的虚表指针
	PrintVFTable((VF_PTR*)(*(int*)&b));
	PrintVFTable((VF_PTR*)(*(int*)&d));
	return 0;
}

多继承中的虚函数表

1.多继承的 Derive 多大???

在32位机器下,Base1 的大小是 8 字节(虚表指针和一个int类型的数据),Base2 也是8字节。Derive 继承了两个父类,8+8是两个父类的大小,再加上自己的int大小,而自己的虚函数会继承父类的虚表,也就是说会使用父类中先继承下来的虚表。直接把自己的虚函数添加到该虚表中,所以没有额外的大小。所以Derive的大小在32位下是 20 字节,在64位下是 40字节(都需要内存对齐)。

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;
};
int main()
{
	cout << sizeof(Derive) << endl;
	return 0;
}

func3 去哪里了,监视窗口看不到啊,同样被监视窗口隐藏了。

我们同样自己打印一份真实的虚表,便于观察。

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(*VF_PTR)(); //函数指针类型重定义名称
void PrintVFTable(VF_PTR* pTable)
{
	for (size_t i = 0; pTable[i] != 0; ++i)
	{
		printf("vfTable[%d]:%p->", i, pTable[i]);
		VF_PTR f = pTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	PrintVFTable((VF_PTR*)(*(int*)&d));
	cout << "====================" << endl;  //分割线,便于观察,子类中继承下来的两个虚表
	PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));
	return 0;
}

说明如果是多继承,派生类会优先将重写的虚函数放入先继承的基类对应的虚函数表中,且可能将自身特有的虚函数添加到第一个基类虚函数表后面。


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

一、选择题

1.下面程序输出的结果是()

#include<iostream>
using namespace std;
class A {
public:
	A(const char* s) { cout << s << endl; }
	~A() {}
};
class B :virtual public A
{
public:
	B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
	C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
	D(const char* s1, const char* s2, const char* s3, const 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 

2. 多继承中指针偏移问题?下面说法正确的是()

 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 

3. 以下程序输出结果是什么()

class A 
{
public:
	virtual void func(int val = 1) 
	{
		cout << "A->" << val << endl;
	}
	virtual void test() //  A* this 
	{
		func(); // A->func()
	}
};
class B : public A 
{
public:
	void func(int val = 0) 
	{
		cout << "B->" << val << endl;
	}
};
int main()
{
	B* p = new B;
	p->test(); //p->test(p);
	return 0;
}

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

二、问答题

1. 什么是多态?

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

3. 多态的实现原理?

4. inline函数可以是虚函数吗?

5. 静态成员可以是虚函数吗?

6. 构造函数可以是虚函数吗?

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

8. 对象访问普通函数快还是虚函数更快?

9. 虚函数表是在什么阶段生成的,存在哪的?

10. C++菱形继承的问题?虚继承的原理?

11. 什么是抽象类?抽象类的作用?


http://www.niftyadmin.cn/n/5690570.html

相关文章

Java--IO基本流

IO流 概述 生活中&#xff0c;你肯定经历过这样的场景。当你编辑一个文本文件&#xff0c;忘记了ctrls &#xff0c;可能文件就白白编辑了。当你电脑上插入一个U盘&#xff0c;可以把一个视频&#xff0c;拷贝到你的电脑硬盘里。那么数据都是在哪些设备上的呢&#xff1f;键盘…

第二十一章 (动态内存管理)

1. 为什么要有动态内存分配 2. malloc和free 3. calloc和realloc 4. 常⻅的动态内存的错误 5. 动态内存经典笔试题分析 6. 总结C/C中程序内存区域划分 1.为什么要有动态内存管理 我们目前已经掌握的内存开辟方式有 int main() {int num 0; //开辟4个字节int arr[10] …

基于vue框架的大学生四六级学习网站设计与实现i8o8z(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;学生,训练听力,学习单词,单词分类,阅读文章,文章类型,学习课程 开题报告内容 基于Vue框架的大学生四六级学习网站设计与实现开题报告 一、研究背景与意义 随着全球化进程的加速和国际交流的日益频繁&#xff0c;英语作为国际通用语言…

rabbitmq消费者应答模式

1.应答模式 RabbitMQ 中的消息应答模式主要包括两种&#xff1a;自动应答&#xff08;Automatic Acknowledgement&#xff09;和手动应答&#xff08;Manual Acknowledgement&#xff09;。 自动应答&#xff1a; 不在乎消费者对消息处理是否成功&#xff0c;都会告诉队列删…

简单易懂的springboot整合Camunda 7工作流入门教程

简单易懂的Spring Boot整合Camunda7入门教程 因为关于Spring Boot结合Camunda7的教程在网上比较少&#xff0c;而且很多都写得有点乱&#xff0c;很多概念写得太散乱&#xff0c;讲解不清晰&#xff0c;导致看不懂&#xff0c;本人通过研究学习之后就写出了这篇教学文档。 介…

Netty:高性能异步网络编程框架全解析

Netty作为一个基于Java NIO技术的开源异步事件驱动网络编程框架,已经成为开发高性能、高可靠性网络应用的首选工具之一。本文将全面介绍Netty的核心特性、架构原理以及使用方法,帮助你快速掌握这个强大的框架。 Netty的主要特点 异步事件驱动模型 Netty采用异步非阻塞的IO模型…

若依--文件上传前端

前端 ry的前端文件上传单独写了一个FileUpload.Vue文件。在main.js中进行了全局的注册&#xff0c;可以在页面中直接使用文件上传的组件。全局导入 在main.js中 import 组件名称 from /components/FileUpLoadapp.compoent(组件名称) //全局挂载组件在项目中使用 组件命令 中…

音视频入门基础:FLV专题(9)——Script Tag简介

一、SCRIPTDATA 根据《video_file_format_spec_v10_1.pdf》第75页到76页&#xff0c;如果某个Tag的Tag header中的TagType值为18&#xff0c;表示该Tag为Script Tag&#xff08;脚本Tag&#xff0c;又称Data Tag、SCRIPTDATA tag&#xff09;。这时如果Filter的值不为1表示未加…