- 多态的核心就是通过基类访问派生类定义的成员函数.多态性使得程序调用的函数在运行时动态确定,而不是编译时静态确定的.在使用一个基类类型的指针或者引用,来指向子类对象,进而调用由子类复写的个性化的虚函数.
- 而虚函数是实现多态的机制,在类成员方法的声明(不是定义)语句前面加上
virtual
,如: virtual void func()- 纯虚函数,在虚函数后面加上 “=0”,比如virtual void func() = 0
- 对于纯虚函数,子类必须提供纯虚函数的个性化实现
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: [email protected]
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include
using namespace std;
class Base
{
public:
Base()
{
cout << "Base() 的构造函数被调用!" << endl;
basePStr = nullptr;
}
virtual void test()
{
cout << "Base() 的虚函数被调用!" << endl;
}// 定义一个虚函数
private:
char *basePStr;
};
int main()
{
// 虚函数和普通的函数在调用的时候没什么区别,父类也可以正常使用
Base b;
b.test();
system("pause");
return 0;
}
上述代码定义了一个虚函数,可以在子类中重新定义父类的做法这种行为称为覆盖(override),或者重写.
- 声明基类指针或者引用,指向任意一个子类对象,如果是实现的虚函数的覆盖,则调用的是子类的虚函数
- 如果没有使用虚函数,则只是简单的覆盖,这样用基类指针在调用相应的函数的时候,总是被限定为基类的函数本身,而无法调用到子类中被重写过的函数.
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: [email protected]
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include
using namespace std;
class Base
{
public:
Base()
{
cout << "Base() 的构造函数被调用!" << endl;
}
void normal_func(void)
{
cout << "Base() 的普通成员函数被调用!" << endl;
}
virtual void vir_func(void)
{
cout << "Base() 的虚函数被调用!" << endl;
}
};
class Derived :public Base
{
public:
Derived()
{
cout << "Derived() 的构造函数被调用!" << endl;
}
void normal_func(void)
{
cout << "Derived() 的普通成员函数被调用!" << endl;
}
virtual void vir_func(void)
{
cout << "Derived() 的虚函数被调用!" << endl;
}
};
int main()
{
Base b;
Derived d;
Base *p = &b; // 指针指向基类
p->normal_func(); // 调用基类的普通成员函数
p->vir_func(); // 调用基类的虚函数
cout << "====================================" << endl;
p = &d; // 指针指向派生类
p->normal_func();// 调用基类的普通成员函数,普通成员函数不管指针的指向,只看指针的类型
p->vir_func(); // 调用派生类的虚函数
system("pause");
return 0;
}
在派生类中声明了一个与基类的某个虚函数具有相同签名的成员函数,不小心重写了这个虚函数.相同签名是指:函数名,参数列表,以及是否是const属性(也就是常函数)
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: [email protected]
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include
using namespace std;
class Base
{
public:
virtual void print_info()
{
cout << "Base::print_info() 被调用!" << endl;
}
};
class Derived :public Base
{
public:
void print_info()
{
cout << "Derived::print_info() 被调用!" << endl; // 无意的重写
}
};
int main()
{
system("pause");
return 0;
}
函数的签名包括: 函数名,参数列表,以及是否具有const属性.如果这些不匹配,会意外的创建一个新的虚函数,而不是重写了一个已经存在的虚函数.
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: [email protected]
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include
#include
using namespace std;
class Base
{
public:
virtual void print_info(string name)
{
cout << "Base::print_info() 被调用!" << endl;
}
};
class Derived :public Base
{
public:
virtual void print_infoo(string name)
{
// 多了一个o,函数名不同,重写失败,重新创建了一个新的虚函数
}
virtual void print_info(int age)
{
//函数名相同,但是参数不同,没有重写,重新创建了一个新的虚函数
}
virtual void print_info(string name)const
{
// 函数名相同,参数相同,但是const的属性不一致,重写失败,重新创建了一个新的虚函数.
}
};
int main()
{
system("pause");
return 0;
}
这三种写法编译器都不会报错,因为编译器不知道你是要重新创建一个新的函数,还是要覆盖基类的函数进行重写.
使用override以及final关键字可以避免上述两个错误
override:
保证派生类中声明的重载函数,与基类的虚函数具有相同的签名.一旦定义了override
属性,就证明这个函数是重写的基类的虚函数final:
阻止类的进一步派生和虚函数的进一步重写.定义了final
属性,就证明该类禁止派生,或者该函数不能再进行重写,否则编译器就报错了.
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: [email protected]
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include
using namespace std;
class Base
{
public:
virtual void print_info(string name)
{
// 基类的虚函数
}
};
class Derived :public Base
{
public:
virtual void print_info(string name)const override
{
// 这里会直接报错,用override声明的函数不能重写基类的虚函数.
// 因为这里的函数多了一个const属性
}
};
int main()
{
system("pause");
return 0;
}
final
关键字,加上final
关键字以后,再被继承或者重写,就会报错/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: [email protected]
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include
using namespace std;
class Base
{
public:
virtual void print_info(string name) final
{
// 虚函数,加上final关键字,防止子类进行重写覆盖
}
};
class Derived :public Base
{
public:
virtual void print_info(string name) override
{
// 报错: 无法重写final函数
}
};
int main()
{
system("pause");
return 0;
}
class Simple
{
public:
void func_normal_a(void)
{
// 普通的成员函数a
};
void func_normal_b(void)
{
// 普通的成员函数b
}
private:
int mValA; // 成员变量A
int mValB; // 成员变量B
};
其内存的分部如下:
其中成员函数放在代码区,为该类的所有对象公有.而成员变量则是放在堆区,为每个对象私有.
class Simple
{
public:
void func_normal_a(void)
{
// 普通的成员函数a
};
void func_normal_b(void)
{
// 普通的成员函数b
}
virtual void func_virtual(void)
{
// 虚函数
}
private:
int mValA; // 成员变量A
int mValB; // 成员变量B
};
上图的虚指针的意识是虚函数表的指针.
这时候去sizeof一个类的对象的大小的时候,会发现类对象大了4个字节.多出来的4个字节就是实现虚函数的关键(虚函数表指针).这个指针指向一张名为虚函数表(vtbl)的表,而表中的数据则为函数指针,存储了虚函数func_b()具体实现所对应的位置.注意,普通函数,虚函数,虚函数表都是同一个类的所有的对象公有的,只有成员变量和虚函数表指针是每个对象私有的.当类中有多个虚函数时,仍然只有一个虚函数表指针vptr,而此时的虚函数表vtbl中会有多个函数指针,分别指向对应的虚函数实现区域.
虚函数的实现过程:
通过对象内存中虚函数指针vptr找到虚函数表vtbl,再通过vtbl中的函数之战找到对应虚函数实现区域并进行调用.所以虚函数的调用是由指针所指向内存块的具体类型决定的.
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但是要求任何的派生类都要定义自己的特性实现.在基类中实现虚函数的方法是在虚函数的声明基础上加上=0:
virtual void func(void) = 0; // 纯虚函数
很多的情况下,基本本身生成对象并没有实际的意义.例如,一个动物的基类可以派生出其他的动物,比如鸽子,猫,狗,这些派生出来的类都可以创建实例,但是创建一个动物的实例本身没有意义.这个时候可以把基类定义为纯虚函数,它就不能被实例化.
定义纯虚函数是未了实现一个接口,用来规范派生类的行为,也即使规范继承这个类的程序员必须实现这个函数.派生类仅仅只是继承函数的接口.纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但基类无法为纯虚函数提供一个合理的缺省实现.所以类纯虚函数的声明就是告诉子类的设计者,你必须为一个纯虚函数提供一个实现,具体怎么实现,我不管.
含有纯虚函数的类称为抽象类,它不能实例化对象,只能创建它的派生类的实例.
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出.如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类.如果派生类中给出了基类的纯虚函数的实现,则这个派生类不再是抽象类了,它是一个可以建立对象的具体的类.
- 基类中的某个成员方法,在大多数情况下都应该由子类提供个性化实现,但是基类也可以提供缺省备选方案的时候,应该设计为虚函数.
- 基类中的某个成员方法,必须由子类提供个性化实现的时候,应该设计为纯虚函数.
- 构造函数不能是虚函数,但是析构函数可以是虚函数并且推荐最好设置为虚函数.
- 虚函数的实现是通过对象内存中的vptr来实现的.而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化的操作.那么再构造函数完成之前,也即还没有进行初始化,此时的vptr是没有值的,也就无法通过vptr找到作为构造函数和虚构函数所在的代码区,所以构造函数只能以普通的函数的形式存放在类所指定的代码区中.
- 而对于析构函数,当我们delete(a)的时候,如果析构函数不是虚函数,那么调用的将会是基类的base的析构哈数.而当继承的时候,通常派生类会在基类的基础上定义自己的成员,因此我们当然希望可以调用派生类的析构函数对新定义的成员进行析构!