目录
一.虚函数
首先来补充一个知识点:在C++中,允许空的结构体和类存在,大小占用一个字节,给出验证代码:
class A
{
};
struct B
{
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
return 0;
}
这一个字节的大小用于标记这是给某个类或者结构体的内存。那这跟虚函数有什么关系呢?哦对,先来介绍一下虚函数,虚函数就是函数前面加个virtual关键字,那虚函数如何使用,又有什么奥秘?接着往下看!
class A
{
virtual void print()
{
cout << "调用了虚函数" << endl;
}
};
class B
{
void print()
{
cout << "调用了普通函数" << endl;
}
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
return 0;
}
可以看到,内置了一个普通函数它的大小还是一,因为没有添加额外的数据成员,但是虚函数却是显示4,这是为什么呢?我们打开调试面板看看里面都有些啥?
可以看到,它里面存了一个指针,所以在32位的电脑上才显示大小为四个字节。那么为什么是个指针呢?它有什么用呢?其实上面这个指针叫做虚表指针,它指向了一张虚函数表,表里面存的都是函数入口,是一个二级指针。既然这样,那我们能否通过这个指针调用虚函数?答案是肯定的。
class A
{
virtual void print1()
{
cout << "调用了虚函数1" << endl;
}
virtual void print2()
{
cout << "调用了虚函数2" << endl;
}
};
int main()
{
//定义一个函数指针类型
typedef void(*PF)();
//取出对象的地址,因为是个二级指针,所以强转一下
A a;
int** vptr = (int**)&a;
//开始访问,注意类型要保持一致
PF fuc1 = (PF)vptr[0][0];
PF fuc2 = (PF)vptr[0][1];
fuc1();
fuc2();
return 0;
}
注意:表中不是直接存放虚函数,而是存放了虚函数的地址,即虚函数指针。虚函数表是在编译阶段(对象构造时)就生成的,一般情况下储存在代码段(常量区)的。至于为什么要这样,我觉得应该是怕函数的类型不一样,指针的基类不同,在你进行+?操作时,你不知道他会怎么走。
二.多态
讲了虚函数的概念,就可以进入多态了。多态的概念其实理解起来很简单,那就是对于同一个方法, 传入不同的对象调用, 会产生不一样的结果,即:多态是不同对象同种行为产生不同状态。
多态的必要性原则:
- 父类必须存在虚函数
- 子类必须用public继承
- 必须使用基类指针或者引用去调用虚函数
class Parent
{
public:
virtual void Play()
{
cout << "大人玩手机电脑" << endl;
}
void print()
{
cout << "爸爸的正常函数" << endl;
}
protected:
};
class Son :public Parent
{
public:
void Play()
{
cout << "小孩玩过家家" << endl;
}
void print()
{
cout << "儿子的正常函数" << endl;
}
protected:
};
int main()
{
//正常访问
Parent parent;
Son son;
parent.Play();
parent.print();
son.Play();
son.print();
cout << endl;
//指针访问,正常赋值
Parent* pP = new Parent;
Son* pS = new Son;
pP->Play();
pP->print();
pS->Play();
pS->print();
return 0;
}
可以看到,正常访问以及指针正常赋值访问的时候,没有出现多态的现象。当我们进行不正常赋值时:
void play(Parent& who)
{
who.Play();
}
int main()
{
//父类对象通过子类赋值
Parent* parent = new Son;
parent->Play();
parent->print();
cout << endl;
Parent p;
Son s;
play(p);
play(s); //父类对象通过子类赋值
return 0;
}
可以看到,有虚函数的就调用了子类中的同名函数,没有虚函数的还是调用父类本身的函数。
总结:有virtual看对象类型,没有virtual看指针。
上面的写的play函数,其实就有统一接口的功能了,如果感受不出来,那我们来看下面的例子:
class Shape
{
public:
virtual void Draw()
{
cout << "绘制图案" << endl;
}
};
class Rect :public Shape
{
public:
void Draw()
{
cout << "画矩形" << endl;
}
};
class Circle :public Shape
{
public:
void Draw()
{
cout << "画圆形" << endl;
}
};
class Triangle :public Shape
{
public:
void Draw()
{
cout << "画三角形" << endl;
}
};
//统一接口
class Tool
{
public:
void draw(Shape* parent)
{
parent->Draw();
}
};
int main()
{
Tool tool;
tool.draw(new Rect);
tool.draw(new Circle);
tool.draw(new Triangle);
return 0;
}
可以看到,用一个tool工具接口就能利用多态调用出行为相同但对象和作用效果不同的函数,当我们想添加功能的时候,我们不用去修改源代码,只需要增加,甚至是最爽的复制粘贴,比如我们想加一个画椭圆,那我们只需要这样:
class Ellipse :public Shape
{
public:
void Draw()
{
cout << "画椭圆" << endl;
}
};
是不是很简单!但这样写代码其实是有隐患的,这里只是为了便于理解而这么写,这里用子类对象初始化父类指针的时候,这里没有进行delete操作,联想到之前的对象创建到死亡它能够调用重写的析构函数来进行内存释放,那在虚函数里是否有同样的操作,答案是肯定的,只是有一些需要注意的点,我们来看代码:
class Parent
{
public:
~Parent()
{
cout << "爸爸的析构函数" << endl;
}
};
class Son :public Parent
{
public:
~Son()
{
cout << "儿子的析构函数" << endl;
}
};
int main()
{
Parent* p = new Son;
delete p;
return 0;
}
可以看到,我们明明new的是son的对象,但是调用的确实父类中的析构函数, 父类指针指向子类对象. 但是无法调用子类对象的析构函数释放资源,这个问题如何解决?
解决方法:将父类的析构函数也写成虚函数,也就是虚析构函数。
class Parent
{
public:
virtual ~Parent()
{
cout << "爸爸的析构函数" << endl;
}
};
class Son :public Parent
{
public:
~Son()
{
cout << "儿子的析构函数" << endl;
}
};
int main()
{
Parent* p = new Son;
delete p;
return 0;
}
可以看到,我们加上virtual之后,原来的代码就会先调用子类的析构函数,再调用父类的析构函数,这样内存就不会泄漏了。
三.纯虚函数
1.什么是纯虚函数
简单来,纯虚函数就是没有函数体的虚函数,具体长这样:
virtual void print() = 0;
注意:虚函数都是在类中的,纯虚函数也不例外!
纯虚函数没有被重写,无论被继承多少次都是纯虚函数,虚函数无论被继承多少次都是虚函数。
2.纯虚函数的应用
这里要介绍一下抽象类的概念:简单来说,具有至少一个纯虚函数的类,叫做抽象类。
抽象类有两个特点:
- 抽象类不能构建对象,也就是无法实例化对象,如果需要实例化对象必须将所有纯虚函数进行重写。
- 抽象类可以构建对象指针
这里以实现栈的不同方式为例子(没有具体实现,只是一个模型帮助理解):
class stack
{
public:
//父类中所有的操作描述好
virtual void push(int data) = 0;
virtual void pop() = 0;
virtual int top() const = 0;
virtual bool empty() const = 0;
virtual int size() const = 0;
};
//子类想要创建对象,必须重写父类的纯虚函数
//抽象数据类型: 具有强迫性,所有子类重写函数必须和父类的一模一样
class arrayStack :public stack
{
public:
void push(int data)
{
}
void pop()
{
}
int top() const
{
}
bool empty() const
{
}
int size() const
{
}
//可以增加别的函数
//可以增加别的成员
protected:
int* array;
};
struct Node
{
int data;
Node* next;
};
class listStack :public stack
{
public:
void push(int data)
{
}
void pop()
{
}
int top() const
{
}
bool empty() const
{
}
int size() const
{
}
protected:
Node* headNode;
};
void testStack(stack* pStack)
{
pStack->push(1);
while (!pStack->empty())
{
cout << pStack->top();
pStack->pop();
}
}
int main()
{
testStack(new arrayStack);
testStack(new listStack);
return 0;
}
四.final与override
-
final 禁止重写:也就是子类中不能存在同名函数
-
override强制重写:标识作用在子类中用于检查父类中是否存在当前的虚函数 不存在就会报错,因为有标识作用,也增加了代码的可读性。
class A
{
public:
virtual void print() final
{
}
};
class B: public A
{
public:
void print()
{
//报错,被final限制了无法同名
}
};
final可以来写一些你不想继承后被重写的函数。
class A
{
public:
virtual void print()
{
}
};
class B: public A
{
public:
void print() override
{
//标识当前的函数是重写的
}
void print1() override
{
//报错,因为父类中找不到对应的虚函数
}
};
override一般用来检查我们写的这个函数是不是重写的,防止粗心写错,也可以增强代码可读性,让别人一目了然这个函数是重写的。