class Base {
public:
void function() {
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
void function() {
std::cout << "Derived" << std::endl;
}
};
int main() {
Derived derived;
Base* base = &derived;
base->function();
}
当我们执行上面的代码时,程序会优先调用基类中的function
output
Base
所以我们得出第一条结论:基类指针(或引用)只能调用基类的成员函数,不能调用派生类的成员
当我们在基类成员函数前添加virtual
关键字时,该函数变为虚函数;此时我们再让基类指针指向派生类对象
class Base {
public:
virtual void function() {
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
void function() {
std::cout << "Derived" << std::endl;
}
};
int main() {
Derived derived;
Base* base = &derived;
base->function();
}
此时,程序会调用派生类的function
output
Derived
所以我们得出第二条结论,基类指针指向派生类对象时,可以调用派生类中与基类虚函数特征相同的成员函数
当更多派生类继承基类时,我们用同样的方法处理基类
class Base {
public:
virtual void function() {
std::cout << "Base" << std::endl;
}
};
class Derived1 : public Base {
public:
void function() {
std::cout << "Derived1" << std::endl;
}
};
class Derived2 : public Base {
public:
void function() {
std::cout << "Derived2" << std::endl;
}
};
class Derived3 : public Base {
public:
void function() {
std::cout << "Derived3" << std::endl;
}
};
int main() {
Derived1 derived1;
Base* base1 = &derived1;
base1->function();
Derived2 derived2;
Base* base2 = &derived2;
base2->function();
Derived3 derived3;
Base& base3 = derived3;
base3.function();
}
output
Derived1
Derived2
Derived3
我们发现,base
同样都为基类指针或引用,而通过该指针或引用调用同一个虚函数,在运行时根据指针指向或引用表现出多种形式
这样的现象,我们称为动态(继承)多态
综上,我们可以总结出多态形成的条件
- 派生类重写 (overwrite/override) 基类虚函数
- 基类指针(或引用)指向子类对象
- 只需要在基类的函数声明中加上
virtual
关键字,函数定义时不能加 - 在派生类中重写虚函数时,函数特征要相同(返回类型,函数名,参数表)
- 当在基类中写了虚函数时,如果派生类没有重写该函数,那么将使用基类的虚函数
- 在派生类中重写了虚函数的情况下,如果想使用基类的虚函数,可以加类名和域解析符
- 如果要在派生类中重新写基类的函数,则将它设置为虚函数;否则,不要设置为虚函数,有两方面的好处:首先效率更高;其次,指出不要重新定义该函数。
下面,我们来通过一个案例深刻体验以下多态的优点
task:实现一个计算器类
#include <iostream>
class Calculator {
public:
int getResult(std::string oper);
void inputNum(int num1, int num2);
int _num1;
int _num2;
};
int Calculator::getResult(std::string oper) {
int result;
if (oper == "+") {
result = _num1 + _num2;
} else if (oper == "-") {
result = _num1 - _num2;
} else if (oper == "*") {
result = _num1 * _num2;
}
return result;
}
void Calculator::inputNum(int num1, int num2) {
_num1 = num1;
_num2 = num2;
}
void test() {
Calculator c;
c.inputNum(10, 10);
std::cout << c._num1 << "+" << c._num2 << "=" << c.getResult("+") << std::endl;
std::cout << c._num1 << "-" << c._num2 << "=" << c.getResult("-") << std::endl;
std::cout << c._num1 << "*" << c._num2 << "=" << c.getResult("*") << std::endl;
}
int main() {
test();
}
output
10+10=20
10-10=0
10*10=100
写上述代码时我们不难发现,如果我们想要拓展新功能,需要修改源码 而真实开发中,我们提倡开闭原则,即对开放拓展,关闭修改
#include <iostream>
class AbstractCalculator {
public:
virtual int getResult() { return 0; }
void inputNum(int num1, int num2) {
_num1 = num1;
_num2 = num2;
}
int _num1;
int _num2;
};
class AddCalculator : public AbstractCalculator {
public:
int getResult() { return _num1 + _num2; }
};
class SubCalculator : public AbstractCalculator {
public:
int getResult() { return _num1 - _num2; }
};
class MulCalculator : public AbstractCalculator {
public:
int getResult() { return _num1 * _num2; }
};
void test() {
AbstractCalculator* cal = new AddCalculator;
cal->inputNum(10, 10);
std::cout << cal->_num1 << "+" << cal->_num2 << "=" << cal->getResult() << std::endl;
delete cal;
cal = new SubCalculator;
cal->inputNum(10, 10);
std::cout << cal->_num1 << "-" << cal->_num2 << "=" << cal->getResult() << std::endl;
delete cal;
cal = new MulCalculator;
cal->inputNum(10, 10);
std::cout << cal->_num1 << "*" << cal->_num2 << "=" << cal->getResult() << std::endl;
delete cal;
}
int main() {
test();
}
output
10+10=20
10-10=0
10*10=100
从上述代码中,我们可以看到:多态的写法虽然代码量更大,但是结构更清晰,功能拓展更方便,可读性强,以及更优雅(我加的
多态到底是如何实现的呢,我们以下面的代码做演示
class Base {
public:
virtual void function1() {
std::cout << "Base" << std::endl;
}
virtual void function2() {
std::cout << "Base" << std::endl;
}
virtual void function3() {
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
void function1() {
std::cout << "Derived" << std::endl;
}
virtual void function2() {
std::cout << "Derived" << std::endl;
}
virtual void function3() {
std::cout << "Derived" << std::endl;
}
};
- 使用vs提供的工具查看基类和派生类的内存结构
基类
class _s__RTTIBaseClassDescriptor size(36):
+---
0 | pTypeDescriptor
8 | numContainedBases
12 | _PMD where
24 | attributes
28 | pClassDescriptor
+---
class _s__RTTIBaseClassArray size(1):
+---
0 | arrayOfBaseClassDescriptors
+---
class Base size(8):
+---
0 | {vfptr}
+---
Base::$vftable@:
| &Base_meta
| 0
0 | &Base::function1
1 | &Base::function2
2 | &Base::function3
派生类
class Derived size(8):
+---
0 | +--- (base class Base)
0 | | {vfptr}
| +---
+---
Derived1::$vftable@:
| &Derived1_meta
| 0
0 | &Derived::function1
1 | &Derived::function2
2 | &Derived::function3
vfptr: virtual function pointer 虚函数(表)指针
由编译器在构造函数中被初始化,指针指向内存中的虚函数表
vftable: virtual function table 虚函数表
由编译器自动构建,表内记录每个虚函数的地址
派生类会继承基类的虚函数表
为什么一定是基类指针或引用指向派生类对象
Base* base1 = &derived;
Base base2 = derived;
上述代码中,仅 base1
会发生多态,而 base2
不会发生,这是为什么呢?
摘自《深度探索C++对象模型》
一个pointer或一个reference之所以支持多态,是因为 它们并不引发内存任何“与类型有关的内存委托操作” ; 会受到改变的,只有 它们所指向内存的大小和解释方式 而已。
编译器在 (1)初始化 和 (2)指定 (assignment) 间做出了仲裁。编译器必须确保如果某个object含有一个或一个以上的vfptr,那些 vfptr的内容不会被base class object初始化或改变。
对这段话的解读就是:
- 指针和引用类型只是要求了基地址和这种指针所指对象的内存大小,与对象的类型无关,相当于把指向的内存解释成指针或引用的类型。
- 而把一个派生类对象直接赋值给基类对象,就牵扯到对象的类型问题,为了保证基类对象的vftable不被改变,基类的vfptr不会替换派生类的vfptr,即赋值后基类对象中的内容仍为基类本身,从而无法实现多态。
补充释义:
静态类型:声明的类型,编译期确定。如*base1
,base2
的静态类型都是Base
。
动态类型:在内存中实际的类型。*base1
的动态类型是Derived
,发生动态绑定;而base2
的动态类型仍是Base
,发生类型转换。
动态绑定:即静态类型与动态类型不同,改变指针指向时实际改变的是内存的解释方式修改静态类型。
多态是如何发生的(虚机制)
- 继承时,派生类先调用基类构造函数,再调用自身构造函数,继承得到的vfptr被修改,指向派生类的vftable
- 当基类指针或引用指向派生类时,派生类的内存被指定 (assignment) 为基类,发生动态绑定;此时基类指针指向的内存中,vfptr指向派生类vftable
- 此时调用的函数即为派生类重写的虚函数
现在,我们可以将C++对象模型完善了
class A{
public:
int _x;
int _y;
static int _s;
void A_function();
virtual void A_virtual_function();
static void static_function();
};
含virtual
的单个对象A
模型
类型记录(空类时占位) | 成员变量(连续存储) | 虚函数表指针 | 虚函数表 | 虚函数 | ||
---|---|---|---|---|---|---|
type_info for A |
int A::_x int A::_y |
A_vfptr → | > | &A::A_virtual_function → |
> | A::A_virtual_function |
静态成员变量(不连续存储) |
---|
static int A::_s |
静态成员函数(不连续存储) |
---|
static void A::static_function |
成员函数(不连续存储) |
---|
void A::A_function() |
继承/多态的对象B
模型
class B : public A {
public:
int B_a;
void B_function();
void virtual_function();
virtual void B_virtual_function();
}
类型记录(空类时占位) | 成员变量(连续存储) | 虚函数表指针 | 虚函数表 | 虚函数 | ||
---|---|---|---|---|---|---|
type_info for B |
int A::_x int A::_y int B::_a |
B_vfptr → | > | &B::A_virtual_function →&B::B_virtual_function → |
> > |
B::A_virtual_function B::B_virtual_function |
静态成员变量(不连续存储) |
---|
static int A::_s |
静态成员函数(不连续存储) |
---|
static void A::static_function |
成员函数 (不连续存储) |
---|
void A::A_function() |
void B::B_function() |
- 落灰书目推荐(我正在痛苦阅读中):《深度探索C++对象模型》
纯虚函数是一种特殊的虚函数,当不需要基类虚函数的实现时,可以将虚函数改为纯虚函数
语法
virtual ret_type function(param) = 0;
示例
class Base {
public:
virtual void function() = 0;
};
class Derived1 : public Base {
public:
void function() {
std::cout << "Derived1" << std::endl;
}
};
class Derived2 : public Base {
public:
void function() {
std::cout << "Derived2" << std::endl;
}
};
class Derived3 : public Base {
public:
void function() {
std::cout << "Derived3" << std::endl;
}
};
int main() {
Derived1 derived1;
Base* base1 = &derived1;
base1->function();
Derived2 derived2;
Base* base2 = &derived2;
base2->function();
Derived3 derived3;
Base& base3 = derived3;
base3.function();
}
当类中出现纯虚函数时,该类变为抽象类,不能实例化对象,可以创建指针和引用;
派生类必须重写抽象类的纯虚函数,否则也属于抽象类
- 虚析构
发生多态时,如果需要通过基类指针析构派生类,由于析构函数无法继承,所以靠基类指针是做不到的
class Base {
public:
Base() {
std::cout << "Base constructor" << std::endl;
}
virtual void function() = 0;
~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor" << std::endl;
p = new int(10);
}
void function() {
std::cout << "Derived" << *p << std::endl;
}
~Derived() {
std::cout << "Derived destructor" << std::endl;
delete p;
p = nullptr;
}
int* p;
};
int main() {
Base* base = new Derived;
base->function();
delete base;
base = nullptr;
}
output
Base constructor
Derived constructor
Derived10
Base destructor
基类指针在析构时不会调用派生类析构函数,导致派生类析构函数无法被执行,造成内存泄露
解决方法是将基类析构函数改为虚析构
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
output
Base constructor
Derived constructor
Derived10
Derived destructor
Base destructor
- 纯虚析构
virtual ~Base() = 0;
纯虚析构必须有定义
// 类外
Base::~Base() {
std::cout << "Base destructor" << std::endl;
}
一个类声明了纯虚析构后,该类也变成抽象类,无法实例化对象;也就是说,纯虚析构相比于虚析构仅仅是用来将类变为抽象类的
edit Serein