[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

这篇具有很好参考价值的文章主要介绍了[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

文章目录

  • 前言
  • 一、多态的定义及实现
  • 1.多态的构成条件
  • 2.c++11的override和final
  • 3.重载,重写,重定义的比较
  • 4.抽象类
  • 5.多态的原理
  • 6.多继承中的虚函数表
  • 7.动态绑定和静态绑定
  • 总结

前言

多态的概念:

多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态
举个栗子:比如 买票这个行为 ,当 普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人
买票时是优先买票。
学习多态前我们必须知道虚函数,虚函数的关键字为virtual,虚函数与之前学的虚继承没有任何关系,只是共用了同一个关键字。虚继承是为了解决多继承中数据冗余和二义性的问题,而虚函数是为了实现多态。

一、多态的定义及实现

1.多态的构成条件

多态是在不同继承关系的类对象,去调用同一个函数,产生了不同的行为。

在继承中构成多态有两个必要条件:

1.必须通过基类的指针或者引用调用虚函数

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。重写的三同(函数名,参数,返回值)

下面我们用代码演示一下:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
    //1.不满足多态---看调用者的类型,调用这个类型的成员函数
    //2.满足多态  -- 看指向的对象的类型,调用这个类型的成员函数
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

 以上是我们所举的一个买票的例子,基类中的函数与子类中的函数名相同前面加了virtual关键字修饰并且参数也相同都是无参的,但是实现不同,而我们的func函数是用基类的对象的引用去调用函数,满足了形成多态的条件,当子类对象调用这个函数时我们发现打印的是子类的函数实现,父类对象打印的是父类的函数实现,如下图:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 我们前面学了继承都知道如果用基类对象调用函数只能调用基类的函数,现在基类对象不仅能调用自己的还能调用子类的,下面我们看看如果不是基类的指针或引用是什么现象:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 我们可以看到当不是基类的指针或引用的时候调用同名函数只会调用自己的,下面我们在看看如果不写virtual是什么情况。

首先是基类函数不加virtual:(在修改前一定要恢复为基类的指针或引用)

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 我们可以看到基类中无virtual修饰是形成不了多态的

派生类函数不加virtual:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 我们发现派生类加不加virtual关键字不重要,只要基类同名函数加了virtual就可以形成多态。

我们刚刚所演示的情况用其他方式也能实现,下面我们用一个更详细的例子来展现多态的重要性:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person {
public:
	void BuyTicket() { cout << "买票-半价" << endl; }
	~Student()
	{
		cout << "~Student()" << endl;
	}
};
void Func(Person* p)
{
	p->BuyTicket();
	delete p;
}
int main()
{
	Func(new Person);
	Func(new Student);
	return 0;
}

 以上代码我们加了析构函数,在调用func函数的时候开了基类空间和派生类空间,这个时候析构函数并没有重写加virtual关键字,所以一定是父类指针调用父类的析构函数,如下图:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 这个结果是我们想要的吗?肯定不是!因为子类很有可能有自己的东西,如果我们用父类的析构析构子类对象必定会造成内存泄漏,这里我们想的是父类指针中存放父类对象就调用父类的析构函数,父类指针中存放子类对象就调用子类的析构函数,这个时候我们给基类中的析构函数加上virtual关键字:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

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

 这个时候我们发现正确的释放了子类的资源,为什么会析构完子类再析构父类这个问题我们在继承中已经提到过,因为派生类的构造函数结束的时候自动调用父类的构造函数实现先析构子再析构父。这里不知道会不会有人问为什么析构函数名字不同却实现了多态呢?因为我们之前说过,编译器对于任何一个析构函数的处理都是处理为destructor,可以理解为每个类中的析构函数都是同名函数。 

上面我们说了虚函数的重写有三同,函数名相同,返回值相同,参数相同。但是有两个例外,第一个例外是协变(基类与派生类虚函数返回值类型不同):协变中的返回值不同必须是父子关系的指针或者引用,不可以是其他的:

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

上面代码中我们可以看到派生类对基类中的f()函数进行了重写,但是他们的返回值类型一个是A*一个是B*,我们看看结果:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 没错 这里即使返回值不同也是多态,那么我们看看如果返回值不是父子关系的指针或引用是什么结果:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 我们可以看到直接编译报错了,并且报错提示不是协变。

2.c++11的override 和 final

从上面可以看出, C++ 对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数
名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有
得到预期结果才来 debug 会得不偿失,因此: C++11 提供了 override fifinal 两个关键字,可以帮
助用户检测是否重写。
1. final :修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 在这里我们只需要记住final关键字必须写到函数的参数列表后面。

2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
	virtual void Drive() {}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 下面我们不重写再看看结果:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 3.重载,覆盖(重写),隐藏(重定义)的对比

重载:重载必须是在同一个作用域,也就是说在一个类中可以构成重载,继承体系中不可以构成重载。重载的条件是函数名相同

覆盖(重写):重写就是我们所说的虚函数,重写是出现在继承体系中的派生类中的,只有子类对父类的函数进行重写。条件:函数名,参数,返回值必须相同(除了协变和构造函数)  virtual关键字在基类中的同名函数中必须出现  必须用父类的指针或引用

重定义(隐藏):两个函数必须分别在基类和派生类的作用域

条件:函数名相同       在继承体系中两个同名函数不构成诚谢就构成重定义  隐藏在子类中是默认隐藏父类的方法使用子类的方法。

4.抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象 。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car {
public:
	virtual void Drive() = 0
	{
		cout << "Car" << endl;
	}
};
class Benz :public Car {
public:
	virtual void Drive() {}
};
int main()
{
	Car s;
	return 0;
}

我们可以看到将car类的Drive函数声明为纯虚函数,这个时候Car类是无法实例化对象的:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

一个类中有纯虚函数这个类被称为抽象类,抽象类也会有子类去继承,但是如果子类没有重写纯虚函数的话这个子类也是抽象类,如果重写了纯虚函数那么这个子类就可以正常使用了:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点) 红括号括起来的就是子类重写了父类的纯虚函数,下面我们看看不重写的例子:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 确实如上面所说,子类不重写父类的纯虚函数那么子类也是抽象类。

5.多态的原理

再讲原理之前有一道常考的题,代码如下:

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

上面这个类的大小是多少呢?我们先给出结果:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

结果为什么是8呢?因为Base类中有虚函数,而编译器看到虚函数会给Base类生成一个虚表指针,我们调试看一下:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点) 接下来我们看看派生类当中这个表都放了什么:

首先将刚刚的代码改造一下:

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 Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 我们发现派生类当中也有一个虚表指针,下面我们再用多态的代码来看一下多态的实现原理:

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 main()
{
	Person ps;
	Func(ps);
	Student st;
	Func(st);
	return 0;
}

 还是之前买票的例子我们调试看一下汇编:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点) 上面多态形成的汇编指令,我们再看看不是多态的:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 我们可以看到是多态时候的汇编指令和不是多态时候的汇编指令相差很多,不是多态的汇编就简单的两行,而多态有很多指令。

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 上图中方框圈出来的就是父类中虚函数的地址和子类中虚函数的地址。

通过上面代码的演示,可以看出满足多态以后的系统调用,不是在编译时确定的,是运行起来以后到对象中找的,不满足多态的函数调用是编译时确定好的。 

下面我们再看看同一个类不同对象是否共用同一张虚表:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)我们可以看到同一个类不同对象用的确实是同一张虚表。

通过以上验证我们得到结论:

1.派生类会拷贝基类的虚表

2.同一个类的不同对象用的同一张虚表

1. 派生类对象 d 中也有一个虚表指针, d 对象由两部分构成,一部分是父类继承下来的成员,虚
表指针也就是存在部分的另一部分是自己的成员。
2. 基类 b 对象和派生类 d 对象虚表是不一样的,这里我们发现 Func1 完成了重写,所以 d 的虚表
中存的是重写的 Derive::Func1 ,所以虚函数的重写也叫作覆盖 ,覆盖就是指虚表中虚函数
的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外 Func2 继承下来后是虚函数,所以放进了虚表, Func3 也继承下来了,但是不是虚函
数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 nullptr
5. 总结一下派生类的虚表生成: a. 先将基类中的虚表内容拷贝一份到派生类虚表中 b. 如果派生
类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c. 派生类自己
新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

接下来我们用程序打印虚表:

下面我们用这个代码进行演示:

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;
}

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 这里由于编译器的监视窗口故意隐藏了func3和func4两个函数,我们看不到虚表中这两个虚函数的地址,所以我们将地址打印出来。

打印虚表前先typedef一个函数指针:

typedef void(*VFPTR)();

 这里我们打印地址的思路是:以前我们测试大小端的时候将int的1类型强转为char类型拿到int的第一个字节,然后判断这个字节是否是1如果是就是小端,现在也一样。

void PrintVFTable(VFPTR table[])
{
	cout << " 虚表地址>" << table << endl;
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, table[i]);
		VFPTR f = table[i];
		f();
	}
	cout << endl;

}

 上面这个函数就是打印虚表的,为什么判断条件是table[i]!=nullptr呢?因为在VS系列的编译器中会在函数指针数组的最后放上一个nullptr。

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 那么我们该如何传参呢?根据我们刚刚的思路,因为一个对象的起始4个字节或8个字节是其存放虚表指针的位置,所以在32位下将对象的地址强转为int*也就是4字节就拿到虚表指针(在32位下指针4字节,在64位下指针是8字节)然后解引用就是一个int类型,由于int类型传参传不过去,实际类型是函数指针类型,所以我们在强制转化为函数指针类型。如下图所示:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

int main()
{
	Base b;
	Derive d;
	PrintVFTable((VFPTR*)(*(int*)&b));
	PrintVFTable((VFPTR*)(*(int*)&d));
	return 0;
}

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 当然既然函数指针数组的首元素类型为VFPTR**,所以我们可以直接强转为VFPTR**再解引用
[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 通过以上对虚函数地址的打印我相信大家能更深刻的了解多态。

下面有几个面试容易考的知识点:

1.虚表是在什么阶段生成的?

答:虚表是在编译阶段生成的。

2.对象中虚表指针是在什么时候初始化的?

答:虚表指针是在基类的构造函数的初始化列表中初始化的。

3.虚表是存在哪里的?要回答这个问题我们先验证一下:

int main()
{
	Base b;
	Derive d;
	int x = 0;
	static int y = 0;
	int* z = new int;
	const char* p = "xxxxxxxxxxx";
	printf("栈对象:%p\n", &x);
	printf("静态对象:%p\n", &y);
	printf("堆对象:%p\n", z);
	printf("常量区对象:%p\n", p);
	printf("b对象虚表:%p\n", *((int*)&b));
	printf("d对象虚表:%p\n", *((int*)&d));
	return 0;
}

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 从上图中我们可以看到b对象虚表和d对象虚表与常量区对象的地址挨的非常近,所以虚表是存在常量区(代码段)当中,而从之前查看地址的时候我们也能看到虚表的地址与函数的地址很接近,而函数的地址就存在代码段中。

6.多继承中的虚函数表

首先我们先写一个多继承的代码:

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;
};

Derive继承base1和base2,并且只重写了func1,还多加了一个虚函数func3,这个时候我们有个问题,新增加的func3要放在哪里呢?首先我们要理解,derive继承了base1和base2所以应该有两种虚表,如下图:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 我们发现在这两种虚表中好像并没有看见func3,这是因为编译器将func3隐藏了,我们需要打印虚表来找func3到底存在哪里了。

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

打印第一张虚表的时候还好说直接在对象的最前面4个字节,那么第二张虚表到底在哪呢?因为我们先继承的base1,再继承的base2所以base1过来就是base2的虚表,所以我们直接跳一个base1的虚表就能找到base2,因为我们要从第一个位置开始偏移所以不能像之前那样强转为int*,要强转为char*指针的偏移量才是一个字节,否则如果偏移量是int*的4个字节就走多了就不是base2虚表的起始地址了。如下图:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

运行程序后打印以下的结果:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点) 我们发现新增加的func3虚函数放在第一个虚表里面了,但是有一个问题为什么虚表中重写的func1的地址不一样呢?

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 我们将主函数修改一下去调试验证为什么不一样:

int main()
{
	Derive d;
	Base1* ptr1 = &d;
	Base2* ptr2 = &d;
	ptr1->func1();
	ptr2->func1();
	return 0;
}

 首先这里满足多态,父类的指针或引用调用func1函数,func1函数是虚函数,下面我们调试看一下地址为什么不一样。

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)这是func1调用的汇编代码, 从上图中我们可以看到第一个函数的调用是正常的,找到func1的地址然后进去,下面我们再看看ptr2访问func1函数的结果:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 [C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 [C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 以上是ptr2调用func函数的整体结果,接下来我们把ptr1调用和ptr2调用func1函数做一个对比:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

 我们发现ptr2调用func函数的时候对地址进行了封装,这就好像我们我们要从宁夏到北京,可以坐飞机直接到北京,但是我们先经过河南再从河南转到河北,再从河北到北京,来来回回绕了很多路,为什么要这样呢?我们在刚刚走调用过程的时候看到这样一条指令:sub ecx,ecx存的是谁的地址呢?其实ecx存的是this指针,我们再配合下面这张图理解一下:

[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

因为这里是多态ptr1和ptr2都要调用子类的函数,而ptr1本来就指向这个Derive对象的开始所以调用子类的函数就恰好指向子类对象的开始所以ecx里面存的是this指针就是子类对象的开始,可以直接调用,而到ptr2的时候ecx就是ptr2的值,这个时候this指针指向对象中间的位置根本无法直接调用子类对象的函数,所以刚刚ptr2在跳的时候就是找到子类对象的开始。而这里谁指向子类对象的开始是谁先继承决定的,base1先继承所以base1的指针刚好指针子类对象的开始。

由于菱形继承,菱形虚拟继承太复杂容易出问题,所以菱形继承和菱形虚拟继承的虚表我们就不看了,我们只需要知道:虚表存储虚函数地址        虚基表存储偏移量文章来源地址https://www.toymoban.com/news/detail-421127.html

7.动态绑定和静态绑定

1. 静态绑定又称为前期绑定 ( 早绑定 ) 在程序编译期间确定了程序的行为 也称为静态多态
比如:函数重载
2. 动态绑定又称后期绑定 ( 晚绑定 ) ,是在程序运行期间,根据具体拿到的类型确定程序的具体
行为,调用具体的函数, 也称为动态多态

总结

1. 什么是多态?答:多态分为静态多态和动态多态,静态多态是在编译时期就确定的,比如函数重载。动态多态是在程序运行期间确定的称为动态多态,虚函数可以实现动态多态
2. 什么是重载、重写 ( 覆盖 ) 、重定义 ( 隐藏 ) ?答:重载是在编译时期产生的,函数名相同参数不同则构成函数重载,并且重载是静态时多态。重写(覆盖)是在运行时产生的,由虚函数引起的运行时多态,子类重写父类的函数通过父类指针或引用的方式调用不同形态的同名函数。重定义(隐藏)在继承体系中通常是子类的函数名与父类的函数名相同则在子类中隐藏了父类的方法默认使用子类的方法,如果想要使用父类的则需要域名限定符。
3. 多态的实现原理?答:静态的多态的实现原理是函数名的修饰规则,动态的多态是因为虚表中存放了虚函数的地址在运行的时候会去按条件找对应的方法。
4. inline 函数可以是虚函数吗?答:可以,因为inline对编译器来说只是一个建议,当没有成功变成内联函数时, 这个函数就不再是inline,因为虚函数要放到虚表中去。(真正的内联函数是将函数展开没有地址的,所以不能实现虚函数)
5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有 this 指针,使用类型 :: 成员函数
的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表
阶段才初始化的。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析
构函数定义成虚函数。在子类开了空间的情况下让析构函数变成虚函数就可以成功释放子类的资源不造成内存泄漏
8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针
对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函
数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况
下存在代码段 ( 常量区 ) 的。
10. C++ 菱形继承的问题?虚继承的原理?答:菱形继承会引发数据冗余和二义性的问题,通过虚继承可以解决这个问题,但还是不建议用菱形继承。注意这里不要把虚函数表和虚基表搞混了,虚函数表是存放虚函数的地址,而虚基表是存放偏移量的
11. 什么是抽象类?抽象类的作用?答:类中有纯虚函数不能实例化出对象的类叫抽象类 。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。
12.拷贝构造和赋值可以是虚函数吗?不可以!拷贝构造不能的原因和构造函数一样,而虚函数的作用是完成重写,赋值在定义中没有要求不能是虚函数,但是不建议将赋值定义为虚函数,因为赋值本来就是子类用父类的方法,如果赋值定义为虚函数就是子类调用子类的赋值就不是子类对父类的方法重写了。

到了这里,关于[C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【数据结构】AVL树(万字超详细 附动图)

    一、前言 二、AVL树的性质 三、AVL树节点的定义 四、AVL树的插入 五、AVL树的平衡调整 六、AVL树的验证 6.1 验证有序 6.2 验证平衡 七、AVL树的删除 八、AVL树的性能和代码 还没有学习过二叉搜索树的同学可以移步 【数据结构】二叉搜索树-CSDN博客 https://blog.csdn.net/Eristic0618/arti

    2024年04月25日
    浏览(32)
  • 详细讲解Docker架构的原理、功能以及如何使用

    LXC为Linux Container的简写。可以提供轻量级的虚拟化,以便隔离进程和资源,而且不需要提供指令解释机制以及全虚拟化的其他复杂性。相当于C++中的NameSpace。容器有效地将由单个操作系统管理的资源划分到孤立的组中,以更好地在孤立的组之间平衡有冲突的资源使用需求。

    2024年03月21日
    浏览(45)
  • 【物联网】深入理解CAN通信:原理、应用和实现(超详细,万字警告)

    CAN(Controller Area Network)是一种广泛应用于汽车和工业领域的多节点通信协议。它具有高可靠性、高实时性和抗干扰能力强等特点,能够满足复杂系统中节点之间的数据传输需求。本文将全面介绍CAN通信的原理、应用和实现,并提供实际开发中常用的方法和技巧,帮助读者更

    2024年02月13日
    浏览(40)
  • C++中的多态你真的了解吗?多态原理全面具体讲解

    目录 1. 多态的概念 2. 多态的定义及实现 2.1 多态的构成条件 2.2 虚函数 2.3 虚函数的重写 2.4 C++11 override 和 final 2.5 重载、覆盖(重写)、隐藏(重定义)的对比 3. 抽象类 3.1 概念 4. 多态的原理 4.1 虚函数表 4.2多态的原理 4.3 动态绑定与静态绑定 5. 单继承和多继承关系中的虚函数表

    2024年02月04日
    浏览(34)
  • 【数据结构】深入浅出理解快速排序背后的原理 以及 版本优化【万字详解】(C语言实现)

    快速排序是 Hoare 于1962年提出的一种 二叉树结构 的 交换排序 方法。 任取待排序元素序列中的 某元素作为基准值 ,按照该排序码将待排序集合 分割成两子序列 , 左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值 ,然后最左右子序列重复该过程,直到所

    2024年02月05日
    浏览(105)
  • 【数据结构】万字超详解顺序表(比细狗还细)

    我这个人走得很慢,但是我从不后退。                                ——亚伯拉罕·林肯   目录 一.什么是线性表? 二.什么是顺序表? 三.接口函数的实现 1.创建工程 2.构造顺序表 3.初始化顺序表 3.初始化顺序表 4.顺序表的尾插 5.顺序表的头插  6.顺序表的尾删  7.顺序

    2023年04月09日
    浏览(41)
  • 【算法与数据结构】归并排序的代码实现(详细图解)以及master公式的讲解

    目录 1、归并排序  1.1、算法描述  1.2、图解说明 2、代码实现  3、master公式 3.1、公式以及结论 3.2、适用于某些特殊的递归 3.3、计算归并排序的时间复杂度 归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用 递归 或者说是 分治法 (Divide and Conquer)的一个非

    2024年02月08日
    浏览(62)
  • 回声状态网络(Echo State Networks,ESN)详细原理讲解及Python代码实现

    回声状态网络是一种循环神经网络。ESN 训练方式与传统 RNN 不同。网络结构如下图: (1)储层(Reservoir):中文翻译有叫储备池、储层、储蓄池等等各种名称。ESN 中的储层是互连神经元的集合,其中连接及其权重是随机初始化和固定的。该储层充当动态储层,其目的是将输

    2024年04月17日
    浏览(43)
  • C++类开发第七篇(详细说说多态和编译原理)

    多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将”what”和”how”分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时也能扩展。 c++支持编译时多态(静态多

    2024年03月09日
    浏览(53)
  • stm32步进电机S型加减速程序源码与详细分析,资料为算法实现以及算法的相关讲解

    stm32步进电机S型加减速程序源码与详细分析,资料为算法实现以及算法的相关讲解,例程中有stm32f103步进电机S型加减速的完整工程代码,对步进电机s型加减速控制很有帮助 标题:基于STM32的步进电机S型加减速控制程序源码与详细分析 摘要:本文介绍了一种基于STM32的步进电

    2024年01月25日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包