目录
一.多态的概念
构成多态的两个条件
二.虚函数
1.虚函数的重写(覆盖)
2.虚函数重写的两个例外:
三.C++11的override和final
四.重载、覆盖(重写)、隐藏(重定义)
五.抽象类和接口继承
纯虚函数
接口继承和实现继承
编辑
六. 多态的原理
虚表指针和虚表
动态绑定与静态绑定
七.常见面试题
无论村子有多么黑暗,多少矛盾,我都是木叶的宇智波鼬,我会在暗处守护着木叶
1.虚函数的重写,三同(函数名,参数,返回值)
2.父类指针或引用去调用
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
派生类中继承基类的虚函数可以不加virtual关键字,虽然可以不加,但还是建议大家加上,代码风格更为规范一些。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
//多态的第二个条件,父类指针或引用去调用
void Func(Person& p)
{
p.BuyTicket();
}
//指针
//void Func(Person* p)
//{
// p->BuyTicket();
//}
//普通调用,这个错误
//void Func(Person p)
//{
// p.BuyTicket();//和对象类型有关,不符合多态调用,这里只会调用父类Person的BuyTicket()
//}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
1.协变(基类与派生类虚函数返回值类型不同)
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
2.析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person p1;
Student p2;
return 0;
}
通过new来间接调用构造进行对象的创建,将对象的空间开辟在堆上,而不是开辟在栈上,并且两个对象在new之后都是用基类指针进行接收的,这也很合理, 因为基类指针既可以指向基类对象又可以指向派生类对象,但是delete的时候这里就会出问题,由于析构函数不是虚函数,则调用一定不是多态调用,那就是普通调用,普通调用只和调用对象类型有关,则new出来的Person和Student对象的析构都调用的是Person类的析构,如果派生类没有申请资源还好说,但只要申请资源,则Person的析构是无法完成Student对象资源的清理的,那在进程运行期间就会发生内存泄露,这就出大事情了。
1.final修饰类,类为最终类不能被继承。
如何实现一个不能被继承的类?
1.构造私有,C++98的方式。继承下来的成员不能自己初始化,所以如果父类构造函数私有,则不能被调用
2.类定义时加final,C++11的方式,表示类为最终类,不能被继承。
class A final
{
private:
A()
{}
};
class B :public A
{
};
int main()
{
//B b;
return 0;
}
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
1.override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
virtual void Drive(int x) {}
//这样的基类虚函数,派生类就没有重写,override会检查出来并报错。
//如果不加override,构成隐藏
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
//assert是运行时做的断言检查,override是编译时做的虚函数是否重写检查。
};
int main()
{
Benz b;
}
隐藏可以看作重写的条件的子集。
//抽象类
class Car//抽象类---不能实例化出对象
{
public:
//纯虚函数所在类为抽象类
virtual void Drive() = 0 //一般纯虚函数不写实现,写了也没啥用,因为其所在类无法实例化出对象
{
cout << "endl;" << endl;
} ;
};
class Benz :public Car
{
public:
//如果不重写纯虚函数,则自然继承下来之后,派生类也会变为抽象类,自然也不能实例化出对象。
//只有对纯虚函数进行重写之后,函数就不算纯虚函数了,派生类就不再是抽象类,就可以实例化出对象。抽象类强制子类重写纯虚函数
virtual void Drive()//重写纯虚函数
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
int main()
{
BMW b;//如果BMW没有重写纯虚函数,则继承下来的纯虚函数就是原生的,那么BMW就是抽象类,抽象类是不能实例化对象的。
//抽象类从某种程度上说,就是强迫子类重写纯虚函数。
//override是检查虚函数是否重写,抽象类是强迫派生类重写纯虚函数,否则派生类无法实例化出对象。
}
首先,对于非静态类成员函数的调用,无论是对象去调用函数,还是函数之间进行调用,他们本质都是通过隐含的this指针来完成调用的,那么B继承A中的test()后,并未显示给出,则不满足重写定义,所以test()的this指针类型还是原来基类类型的也就是A *,那么当test()里面调用func时,就是通过A *的this指针进行调用,而func又是虚函数的重写,所以此时就满足多态调用,并且A *this指针指向的对象是B对象,那么调用的func函数就是B类的func函数,而多态调用下的虚函数继承又是接口继承,所以使用的val是1,那么最终的打印结果就是B→1。
总结就是这个符合多态而且而多态调用下的虚函数继承又是接口继承,继承基类的接口,所以是B→1。
首先来看一道内存对齐的题目
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1; //0-3
char _ch; // 3-4 最后是0-4 5 对齐到4的整数倍 8
};
int main()
{
cout<
调试看一下里面有一个vfptr指针,这个指针是虚函数表指针
虚表指针是指向函数指针数组的指针,虚表指针在对象里面,对象中虚表指针在什么时候初始化的?在构造函数初始化列表
虚函数表本质是函数指针数组,虚表在代码段,虚表在编译时候产生的
虚函数和普通函数一样的,都是存在代码段的
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;
}
1.派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是另一部分是自己的成员2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
4.总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
动态绑定(Dynamic Binding)和静态绑定(Static Binding)是与多态性相关的两个重要概念,用于描述在编译时和运行时如何决定调用哪个函数或方法
1.静态绑定(Static Binding): 静态绑定是在编译时(编译阶段)确定调用哪个函数或方法的过程。在静态绑定中,编译器会根据调用表达式中的信息,确定要调用的函数,这通常发生在编译器生成目标代码时。静态绑定适用于非虚函数,普通的函数重载,以及运算符重载等。
class Base {
public:
void print() {
cout << "Base class" << endl;
}
};
class Derived : public Base {
public:
void print() {
cout << "Derived class" << endl;
}
};
int main() {
Derived d;
Base& b = d;
b.print(); // 静态绑定,编译时确定调用 Base::print()
}
在上述例子中,尽管 b
引用的是 Derived
类的对象,但因为 print
函数不是虚函数,所以在编译时就已经确定了调用 Base::print()
。
2.动态绑定(Dynamic Binding): 动态绑定是在运行时(运行阶段)根据对象的实际类型来决定调用哪个函数或方法的过程。这通常涉及虚函数的使用,其中基类中声明的函数被标记为 virtual,并且派生类中进行了重写。运行时会根据对象的实际类型(而不仅仅是引用或指针的类型)调用适当的函数。
class Shape {
public:
virtual void draw() {
cout << "Drawing a shape." << endl;
}
};
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing a circle." << endl;
}
};
int main() {
Circle c;
Shape& s = c;
s.draw(); // 动态绑定,运行时根据实际对象类型调用 Circle::draw()
}
在上述例子中,由于 draw
函数被声明为虚函数,调用 s.draw()
会根据实际对象类型 Circle
调用 Circle::draw()
。
总结
重载:
两个函数必须在同一作用域里面,满足函数名相同,参数类型,个数,顺序不同时即为函数重载。
隐藏:
两个函数分别处于不同的作用域,只要函数名相同就构成隐藏,在访问时如果不指定基类作用域限定符,则默认调用的同名函数为派生类类域。
重写:
两个函数分别处于不同的作用域,两个函数必须都为虚函数,且需要满足三同,这样才可以构成重写。协变和析构函数算是重写的特殊情况,另外子类中的虚函数可以不加virtual关键字。
this
指针,使用类型::
成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
vptr
需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造函数。
8.对象访问普通函数快还是虚函数更快?
9.虚函数表是在什么阶段生成的,存在哪的?
虚函数表在程序编译期间生成,虚表指针在构造函数的初始化列表阶段进行初始化,vs上面虚表和虚函数都存在于代码段
10.什么是抽象类?抽象类的作用?
纯虚函数所在的类称之为抽象类。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系