今天,带来C++多态的讲解。
多态和继承并用,能产生“魔法般的效果”。
*文中不足错漏之处望请斧正!
见见多态
是什么
使得父类指针或引用有多种形态。
怎么使它有多种形态呢?咱们先见见猪跑。
见见猪跑
class Base
{
public:
virtual void print() { cout << "Base" << endl;}
};
class Derive1 : public Base
{
public:
virtual void print() { cout << "Derive1" << endl;}
};
class Derive2 : public Base
{
public:
virtual void print() { cout << "Derive2" << endl;}
};
int main()
{
Base b;
Derive1 d1;
Derive2 d2;
Base* ptr;
ptr = &b;
ptr->print();
ptr = &d1;
ptr->print();
ptr = &d2;
ptr->print();
return 0;
}
Base
Derive1
Derive2
父类指针,存父类对象地址,就能调用父类中的print,存子类对象地址,就能调用子类中的print,拥有了多种形态。
多态的实现
满足多态的前提:子类对父类的虚函数完成重写。
啥是虚函数,啥是重写?
虚函数
虚函数是被virtual修饰的成员函数。可以理解为一种对函数体的泛化。
在声明虚函数的域,默认有一份实例;在此域之外,你可以对虚函数“实例化”,得到新的一份实例。
其作用是实现多态性。
重写/覆盖
重写就是“实例化”虚函数,可以产生新的“虚函数实例”,会把原来的实例覆盖掉。
- 对虚函数重写的条件:[函数名、返回值、参数]和父类的虚函数相同
- *重写的仅仅是函数体,重写前后接口是一样的
- 例外
- 子类的虚函数可以不写virtual
- 协变:返回值可以是任意父子类关系的指针/引用
class Base
{
public:
virtual void print() { cout << "Base" << endl;}
};
class Derive1 : public Base
{
public:
virtual void print() { cout << "Derive1" << endl;}
};
class Derive2 : public Base
{
public:
virtual void print() { cout << "Derive2" << endl;}
};
int main()
{
Base b;
Derive1 d1;
Derive2 d2;
Base* ptr;
ptr = &b;
ptr->print();
ptr = &d1;
ptr->print();
ptr = &d2;
ptr->print();
return 0;
}
- print这个虚函数默认有一份实例,它的函数体功能是打印"Base"的
- print这个虚函数在Derive1中被重写了,这份新的实例把原来默认的覆盖了,它的函数体功能是打印”Derive1”的
- print这个虚函数在Derive2中被重写了,这份新的实例把原来默认的覆盖了,它的函数体功能是打印”Derive2”的
如果不重写呢?
class Base
{
public:
virtual void print() { cout << "Base" << endl;}
};
class Derive1 : public Base
{
public:
// virtual void print() { cout << "Derive1" << endl;}
};
class Derive2 : public Base
{
public:
// virtual void print() { cout << "Derive2" << endl;}
};
main函数不变,结果如下:
Base
Base
Base
看上面这个调用的手法,有个疑问:它是怎么知道调用哪个虚函数的实例的?
这就要说到虚函数表了。
虚函数表
有虚函数的类,其对象都会存一个虚函数表指针vptr,虚函数表vtable是干嘛的?
虚函数表是一个类的虚函数地址表,存放了这个类对某个虚函数的所有实例(对虚函数进行重写的到的真实函数)的地址。说白了,某个类对一个虚函数的实例,是用虚函数表来描述和组织起来的。
- 为提高效率,这个可能高频访问的虚表(虚函数表)指针一般放在对象的头4/8个字节。
- 按我们的说法,虚函数表描述的是整个类对某个虚函数的实例,因此它就像类的static成员一样,属于整个类,所以存放在代码段
大概过程:
- 创建类对象(对象的头4/8个字节存了一个虚表指针)
- 调用对象的某个虚函数实例
- 根据虚表来找到当前类对这个虚函数的实例
普通调用和多态调用
普通调用也叫静态绑定,多态调用也叫动态绑定。
静态绑定:编译时通过调用方类型确定调用的函数
动态绑定:运行时通过父类指针指向的对象类型确定调用的函数
- 用父类指针或引用调用被重写的虚函数是动态绑定
- 其他都是静态绑定
多态中的析构函数
若在继承中出现这样的情况:
- 动态申请对象
- 子类没有重写析构函数(静态绑定)
则delete动态对象的空间时,析构调用不完全——只会根据指针类型静态绑定,只调用父类的析构。
class Base
{
public:
~Base()
{
cout << "~Base()" << endl;
delete[] _pb;
}
private:
int* _pb = new int[10];
};
class Derive : public Base
{
public:
~Derive()
{
cout << "~Derive()" << endl;
delete[] _pd;
}
private:
int* _pd = new int[20];
};
int main()
{
Base* ptr = new Derive;
delete ptr;
}
~Base()
可以看到,只会调用父类析构。
静态绑定不行,我们来动态绑定,多态上场。
class Base
{
public:
virtual ~Base()
{
cout << "~Base()" << endl;
delete[] _pb;
}
private:
int* _pb = new int[10];
};
class Derive : public Base
{
public:
~Derive() //此处可以不用写virtual,这是子类可以不用写virtual的一种使用场景
{
cout << "~Derive()" << endl;
delete[] _pd;
}
private:
int* _pd = new int[20];
};
int main()
{
Base* ptr = new Derive;
delete ptr;
}
~Derive()
~Base()
满足了多态,调用完父类析构之后就会自动调用子类析构,成功解决。
Destructor
我们之前提到继承中的析构函数名都会被处理成Destructor,为了能够满足重写的条件。
继承中的对象模型
- 单继承(无重写)的虚函数表:
- 虚函数按照其声明顺序放于表中
- 父类的虚函数在子类的虚函数前面
- 单继承(有重写)的虚函数表
- 被重写的虚函数被放到了虚表中原来父类虚函数的位置(所以调用的时候不会跑去调用父类的,而是调用自己的)
- 没有被覆盖的函数依旧
- 多继承(无重写)的虚函数表
- 每个父类都有自己的虚表
- 子类的成员函数被放到了第一个父类的表中(所谓的第一个父类是按照声明顺序来判断的)
final和override
final
表明类不能被继承。
设计一个不能被继承的类
- final修饰类
class A final {};
class B : public A {};
int main()
{
B b; //A被final修饰,无法被继承
return 0;
}
- 构造私有化——子类想实例化必须调用父类构造。
class A
{
private:
A() {}
};
class B : public A {};
int main()
{
B b; //A的构造是私有,实例B需要调用A的析构,调不动,所以A无法被继承
return 0;
}
override
override可以检查子类虚函数是否和父类的某个虚函数构成重写,或者强制要求某个函数被重写(需要重写的就都带上)。
//override:检查子类的虚函数是否完成重写
class Car
{
public:
void Drive() {}
};
class Benz : public Car
{
public:
//err:'Drive' marked 'override' but does not override any member functions
virtual void Drive() override { cout << "Benz->comfortable" << endl; }
};
int main()
{
Benz mycar;
mycar.Drive();
return 0;
}
抽象类
先导:纯虚函数
是什么:虚函数后写上0,不需要函数体的虚函数。(可以有函数体,但不会被执行)
virtual void func() = 0;
是什么
有纯虚函数的类。
为什么
用于接口继承。
- 接口继承:主要是为了让子类重写,形成多态
- 实现继承:主要是为了复用函数体
class Car
{
public:
virtual void Drive() = 0;
};
class BMW : public Car
{
public:
virtual void Drive()
{
cout << "驾驶乐趣+1" << endl;
}
};
int main()
{
BMW mycar;
mycar.Drive();
return 0;
}
特性
- 抽象类不能实例化出对象
- 若想实例化对象,必须重写纯虚函数(这样就不是抽象类了)
- 子类继承了抽象类也还是抽象类
今天的分享就到这里了,感谢您能看到这里。
这里是培根的blog,期待与你共同进步!