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

news2024/10/5 0:11:14

目录

多态的概念

多态的定义及实现

多态的构成条件

虚函数

虚函数的重写

虚函数重写的两个例外:

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.coloradmin.cn/o/2188963.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

绝对值得收藏!分享7款ai写作论文免费一键生成网站

在当前的学术研究和写作过程中&#xff0c;AI写作工具已经成为了许多研究者和学生的重要助手。这些工具不仅能够提高写作效率&#xff0c;还能帮助生成高质量的论文内容。以下是七款免费的AI写作论文生成器&#xff0c;其中特别推荐千笔-AIPassPaper。 1.千笔-AIPassPaper 千…

信号处理: Block Pending Handler 与 SIGKILL/SIGSTOP 实验

1. 信号处理机制的 “三张表” kill -l &#xff1a;前 31 个信号为系统标准信号。 block pending handler 三张表保存在每个进程的进程控制块 —— pcb 中&#xff0c;它们分别对应了某一信号的阻塞状态、待处理状态以及处理方式。 block &#xff1a;通过 sigset_t 类型实现&…

YOLO11改进 | 检测头 | 融合渐进特征金字塔的检测头【AFPN3】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 本文介绍了一个渐进特征金字塔网络&…

关于 S7 - 1200 通过存储卡进行程序更新

西门子S7-1200系列PLC可以通过存储卡进行程序的更新&#xff0c;固件版本的升级以及程序数据的存储多项功能。本例进行程序更新的操作。 存储卡的订货号以及存储容量 一&#xff1b;如何插入存储卡 在CPU断电下&#xff0c;将CPU上挡板向下掀开&#xff0c;可以看到右上角有一…

ai写作论文会被检测吗?分享市面上7款自动写论文网站

近年来&#xff0c;随着人工智能技术的飞速发展&#xff0c;AI写作工具在学术界引起了广泛关注。然而&#xff0c;这些工具的使用也引发了关于学术诚信和检测机制的讨论。根据多所高校的声明&#xff0c;为了应对AI代写论文的现象&#xff0c;许多高校已经开始引入论文检测工具…

Python入门:深入了解__init__.py 文件(如何实现动态导入子模块)

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 `__init__.py` 的作用示例:📝 如何编写 `__init__.py`1. 空的 `__init__.py`2. 导入子模块3. 初始化代码4. 动态导入子模块📝 编写 `__init__.py` 的技巧和注意事项⚓️ 相关链接 ⚓️📖 介绍 📖 在…

01:(寄存器开发)点亮一个LED灯

寄存器开发 1、单片机的简介1.1、什么是单片机1.2、F1系列内核和芯片的系统架构1.3、存储器映像1.4、什么是寄存器 2、寄存器开发模板工程3、使用寄存器点亮一个LED4、代码改进15、代码改进2 本教程使用的是STM32F103C8T6最小系统板&#xff0c;教程来源B站up“嵌入式那些事”。…

前缀和(6)_和可被k整除的子数组_蓝桥杯

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 前缀和(6)_和可被k整除的子数组 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 …

kubeadm部署k8s

1.1 安装Docker [rootk8s-all ~]# wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.huaweicloud.com/docker-ce/linux/centos/docker-ce.repo [rootk8s-all ~]# sed -i sdownload.docker.commirrors.huaweicloud.com/docker-ce /etc/yum.repos.d/docker-ce.repo [ro…

基于Keras的U-Net模型在图像分割与计数中的应用

关于深度实战社区 我们是一个深度学习领域的独立工作室。团队成员有&#xff1a;中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等&#xff0c;曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万粉丝&#xff0c;拥有2篇国家级人工智能发明专利。 社区特色&a…

Yocto - 使用Yocto开发嵌入式Linux系统_07 构建使用的临时文件夹

Detailing the Temporary Build Directory 在本章中&#xff0c;我们将尝试了解映像生成后临时构建目录的内容&#xff0c;并了解 BitBake 如何在烘焙过程中使用它。此外&#xff0c;我们还将了解这些目录中的某些内容如何在出现问题时作为有价值的信息来源来帮助我们。 In thi…

前缀和——从LeetCode题海中总结常见套路

目录 前缀和定义 截断前缀和DP&#xff1a;LeetCode53.最大子序和 经典左右指针&#xff1a;LeetCode209.长度最小的子数组 暴力求解&#xff1a;超时 优雅的双指针写法一&#xff1a; 优雅的双指针写法二&#xff1a; LeetCode.1588.所有奇数长度子数组的和 手速题&am…

springboot系列--web相关知识探索三

一、前言 web相关知识探索二中研究了请求是如何映射到具体接口&#xff08;方法&#xff09;中的&#xff0c;本次文章主要研究请求中所带的参数是如何映射到接口参数中的&#xff0c;也即请求参数如何与接口参数绑定。主要有四种、分别是注解方式、Servlet API方式、复杂参数、…

[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解

[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解 前言: 古人云: 得卧龙者&#xff0c;得天下。 然在当今大语言模型流行的时代&#xff0c;同样有一句普世之言: 会微调技术者&#xff0c;得私域大模型部署之道&#xff01; 在众多微调技术中&#xff0c;LoRA (…

单细胞scDist细胞扰动差异分析学习

scDist通过分析不同状态下细胞的距离来找到差异最大的细胞亚群(见下图的A)&#xff0c;然后再分析每一个细胞亚群的PCA通过线性的混合模型并结合最终的系数去预估不同干预方式下细胞群之间的距离。 Augur是通过对每一个细胞进行AUC评分并排序最终找到扰动最佳的细胞群&#xf…

等额本金和等额本息是什么意思?

等额本金和等额本息是两种常见的贷款还款方式&#xff0c;它们各自有着不同的特点和适用场景。下面我将用通俗易懂的语言来解释这两种还款方式&#xff1a; 等额本金 定义&#xff1a;等额本金指的是在贷款期限内&#xff0c;每月偿还相同数额的本金&#xff0c;而利息则随着剩…

FPGA远程烧录bit流

FPGA远程烧录bit流 Vivado支持远程编译并下载bit流到本地xilinx开发板。具体操作就是在连接JTAG的远程电脑上安装hw_server.exe。比如硬件板在实验室或者是其他地方&#xff0c;开发代码与工程在本地计算机&#xff0c;如何将bit流烧录到实验室或者远程开发板&#xff1f; vi…

Socket套接字(客户端,服务端)和IO多路复用

Socket套接字&#xff08;客户端&#xff0c;服务端&#xff09; 目录 socket是什么一、在客户端1. 创建套接字2. 设置服务器地址3. 连接到服务器4. 发送数据5. 接收数据6. 关闭连接 二、内核态与用户态切换三、系统调用与上下文切换的关系四、在服务端1. 创建 Socket (用户态…

【Linux】进程地址空间(初步了解)

文章目录 1. 奇怪的现象2. 虚拟地址空间3. 关于页表4. 为什么要有虚拟地址 1. 奇怪的现象 我们先看一个现象&#xff1a; 为什么父子进程从“同一块地址中”读取到的值不一样呢&#xff1f; 因为这个地址不是物理内存的地址 &#xff0c;如果是物理内存的地址是绝对不可能出…

C++【类和对象】(友元、内部类与匿名对象)

文章目录 1.友元2.内部类3.匿名对象结语 1.友元 友元提供了⼀种突破类访问限定符封装的方式&#xff0c;友元分为&#xff1a;友元函数和友元类&#xff0c;在函数声明或者类声明的前面加friend&#xff0c;并且把友元声明放到⼀个类的里面。外部友元函数可访问类的私有和保护…