Content
C++ 面向对象进阶
继承 - 基础
class Base;
class Type :public Base{
public:
Type(a, b, c):Base(a, b){
// ...
}
};
-
继承方式( 派生类均不可访问基类私有成员 )
-
public(父类访问权限不变)
最常用的方式
-
private(父类访问权限全变成私有)
当不希望本类对象访问基类任何成员时,可以考虑使用 private 继承。
-
protected(父类访问权限全变成保护)
使用 private 继承存在一个严重的问题:
当该派生类进一步派生时,该子类将完全无法访问其父类成员。
故一般使用 protected 进行派生,保证该派生类的可派生特性。
-
继承中的对象模型
-
类结构
class Base{ private: int __private; public: int __public; protected: int __protected; }; class Type :public Base{ public: int __sonPublic; };
-
内存结构
class Base size(12): +--- 0 | __private 4 | __public 8 | __protected +---
class Type size(16): +--- | 0 | +--- (base class Base) 0 | | __private 4 | | __public 8 | | __protected | +--- | 12 | __sonPublic +---
继承中的构造和析构
-
调用父类构造
class Base{ private: int member; public: Base(m): member(m){ // ... } }; class Type :public Base{ private: int sonMember; public: Type(m, m1):Base(m), sonMember(m1){ // ... } };
不显式地调用父类构造时,会隐式调用默认的无参构造。
-
调用顺序
- 父类构造(多继承按照顺序构造)
- 本类构造
- 本类析构
- 父类析构(多继承按照反序析构)
继承中的同名处理
-
同名属性
class Base{ public: int member; Base(m): member(m){} }; class Type :public Base{ public: int member; Type(m, m1):Base(m), member(m1){} }; int main(){ Type obj(100, 200); cout << obj.member << endl; // > 200 cout << obj.Type::member << endl; // > 200 cout << obj.Base::member << endl; // > 100 }
当直接引用该同名成员属性时,会隐式调用本类的成员属性。
也可以通过作用域运算符显式地指定成员属性。
-
同名方法
class Base{ public: void fun(); }; class Type :public Base{ public: // 子类重载 void fun(int a); }; int main(){ Type obj; obj.fun(); // 报错 obj.fun(1); // 调用正常 }
子类重载了父类的方法,父类的方法将被隐藏,无法通过重载调用。
想调用父类方法,可以通过作用域运算符显式指定。
不会继承的函数
- 构造和额析构函数
- 等号操作符重载函数
继承 - 进阶
多继承
class Base1 {
// ...
};
class Base2 {
// ...
};
class Type: public Base1, public Base2 {
// ...
};
-
多继承的二义性
通过作用域访问不同父类的成员。
class Base1 { public: int member; }; class Base2 { public: int member; }; class Type: public Base1, public Base2 { // ... }; int main(){ Type obj; obj.member; // 报错,存在多个父类的 member // 通过作用域访问 obj.Base1::member; obj.Base2::member; }
虚继承
-
引出 - 菱形继承
Grandson 的实例中存在两份基类实例的数据。
class Base { public: int member; }; class Son1: public Base { // ... }; class Son2: public Base { // ... }; class Grandson: public Son1, public Son2 { // ... }; int main(){ }
内存结构
这两份基类数据均可由不同的作用域访问到,且是相互独立的。
class Grandson size(8): +--- | 0 | +--- (base class Son1) // 父类 | | 0 | | +--- (base class Base) // 基类 0 | | | member // 两份基类实例数据 | | +--- | | | +--- | 4 | +--- (base class Son2) // 父类 | | 4 | | +--- (base class Base) // 基类 4 | | | member // 两份基类实例数据 | | +--- | | | +--- | +---
为了解决这个问题,使用虚继承
-
虚继承
当一个类对父类的继承被声明为虚拟的(virtual),它就成为了一个 虚基类。
这里的 虚基类 是指该派生类继承自的基类是虚拟的,而不是说该派生类类是基类。
class Base { public: int member; }; // 虚基类 class Son1 : virtual public Base { // ... }; // 虚基类 class Son2 : virtual public Base { // ... }; class Grandson : public Son1, public Son2 { // ... };
内存结构
class Grandson size(12): +--- | 0 | +--- (base class Son1) // 父类 0 | | {vbptr} // 虚基指针 | +--- | 4 | +--- (base class Son2) // 父类 4 | | {vbptr} // 虚基指针 | +--- | +--- +--- (virtual base Base) // 基类 8 | member +--- // 此处的内存空间并不接壤 // Son1 的虚基表 Grandson::$vbtable@Son1@: 0 | 0 1 | 8 (Grandsond(Son1+0)Base) // 偏移量 // Son2 的虚基表 Grandson::$vbtable@Son2@: 0 | 0 1 | 4 (Grandsond(Son2+0)Base) // 偏移量 vbi: class offset o.vbptr o.vbte fVtorDisp Base 8 0 4 0
可以看到,原本存放基类实例的内存空间被一个 {vbptr} 占用了。
vbptr(virtual base pointer):虚拟基类指针
该指针指向一个 vbtable
vbtable(virtual base table):虚拟基类表,结构是一个数组
这个表中记录着 该虚基指针所在派生类实例与基类实例的偏移量。
注意!虚基表的内存空间与类实例并不接壤。
尝试取到该虚基表:
Grandson obj; // Son1 的虚基表数组 cout << ((int*)*((int*)&obj)) << endl; // Son2 的虚基表数组 cout << ((int*)*((int*)&obj + 1)) << endl;
多态 - 基础
静态多态和动态多态
-
静态多态
编译时多态,静态联编,包括函数重载在内的多态。
-
动态多态
运行时多态,动态联编,通过虚函数实现的多态。
静态和动态多态的本质区别就是 静态联编 和 动态联编。
分别指在编译时绑定函数入口和在运行时寻找函数入口。
动态多态的本质:父类的引用或指针指向了子类对象。从而发生动态多态。
静态联编的例子:
class Person {
public:
int member;
void speak() {
cout << "I'm a person." << endl;
}
};
class Programmer: public Person {
public:
int member;
void speak() {
cout << "Life is shot, I'm a programmer." << endl;
}
};
void doSpeak(Person& person){
person.speak();
}
int main(){
Programmer programmer;
doSpeak(programmer);
// > "I'm a person."
}
I’m a person.
doSpeak 函数的实现在编译期就已经确定了需要调用的方法。
要实现动态多态,只需将基类的方法声明为虚拟的。
virtual void speak() {
cout << "I'm a person." << endl;
}
Life is shot, I’m a programmer.
动态多态原理解析
重复一遍
动态多态的本质:父类的引用或指针指向了子类对象。从而发生动态多态。
上例静态联编的例子,其内存结构如下:
class Programmer size(8):
+---
|
0 | +--- (base class Person)
0 | | member
| +---
|
4 | member
+---
现在将基类的 speak
方法声明为虚拟的:
class Person {
public:
int member;
// 声明为虚拟的
virtual void speak() {
cout << "I'm a person." << endl;
}
};
class Programmer: public Person {
public:
int member;
void speak() {
cout << "Life is shot, I'm a programmer." << endl;
}
};
void doSpeak(Person& person){
person.speak();
}
int main(){
Programmer programmer;
doSpeak(programmer);
// > "Life is shot, I'm a programmer."
}
内存结构
-
父类
class Person size(8): +--- 0 | {vfptr} // 虚函数表指针 4 | member +--- // 此处内存空间并不接壤 Person::$vftable@: // 虚函数表 | &Person_meta | 0
0 | &Person::speak
Person::speak this adjustor: 0
可以发现,基类实例多了一个 {vfptr}
**vfptr**(virtual function pointer):虚拟函数表指针
虚函数表指针指向一张虚函数表
**vftable**(virtual function table):虚拟函数表,结构是一个数组
- 子类
```cpp
class Programmer size(12):
+---
|
0 | +--- (base class Person)
0 | | {vfptr} // 虚函数表指针
4 | | member
| +---
|
8 | member
+---
// 此处内存空间并不接壤
Programmer::$vftable@: // 虚函数表
| &Programmer_meta
| 0
0 | &Programmer::speak
Programmer::speak this adjustor: 0
子类继承后会创建一个新表,该表内会指向基类的虚函数。
若子类重新实现了某些虚函数 ,该表中被重新实现的函数将被覆盖,指向新函数的入口。
这种 重新实现 被称为 重写。被重写的函数的参数列表和返回值类型需要对应完全相同。
尝试取到该函数并调用:
Programmer programmer;
((void (*)())*(int*)*(int*)&programmer)();
// > Life is shot, I'm a programmer.
开闭原则
即对扩展开放,对修改关闭。
修改需求并不应去修改实现,而应对原有类进行派生,重写方法,最后通过多态实现需求。
方便维护方便扩展。
由此引出抽象类。
多态 - 进阶
纯虚函数和抽象类
纯虚函数就是没有定义的函数,仅作为一个接口,为子类重写占位,实现多态。
class Type (){
public:
// 纯虚函数
virtual voie fun() = 0;
};
virtual voie fun() = 0;
告诉编译器在 vftable (虚函数表)中保留一个位置,但不放地址。
当一个类中含有纯虚函数时,这个类就被称为 抽象类,无法进行实例化。
且其派生类必须实现该纯虚函数。
虚析构和纯虚析构
-
虚析构
当父类指针指向子类实例时,该子类实例析构会调用父类析构。
如果子类需要在堆区开辟空间,那么析构时就会造成内存泄漏。
class Base () { public: ~Base(){}; }; class Son (): public Base { public: char* mName; Son(const char* name){ // 构造时在堆区托管了额数据 this->mName = new char[strlen(name)]; strcpy(this->mName, name); } ~Son(){ // 析构时释放堆区数据 delete[] this->mName; }; };
当 Son 类实例析构时,会调用其父类 Base 的析构函数,导致
mName
没有正确释放,从而导致内存泄漏。为了解决这个问题,我们就要使用 虚析构,让子类重写析构。
virtual ~Base(){};
注意!父类虚构是一定会调用的,尽管将父类析构声明为虚拟的,父类也需要做收尾工作。
-
纯虚析构
与纯虚函数类似,当一个类中存在纯虚析构时,该类也是一个抽象类,无法实例化。
但有一点稍稍不同,首先,作为抽象类需要派生才能使用,但任意一个派生类对象在释放时一定会调用父类的构造。
所以 纯虚析构要求有定义,在类内声明,在类外定义。
class Base () { public: virtual ~Base() = 0; // 纯虚析构的声明 }; Base::~Base(){ // 纯虚析构的定义 } class Son (): public Base { public: char* mName; Son(const char* name){ // 构造时在堆区托管了额数据 this->mName = new char[strlen(name)]; strcpy(this->mName, name); } ~Son(){ // 析构时释放堆区数据 delete[] this->mName; }; };
类型转换
-
未发生多态
向下类型转换即子类指针指向父类实例,会导致指针寻址范围较大,不安全。
向上类型转换是安全的。
-
发生了多态
多态是父类的指针或引用指向了子类实例,此时一定是安全的。
静态成员方法实现多态
只有非静态成员方法可以被声明为虚拟的,那么静态成员看起来就无法实现多态,但实际上还是有方法的,这里提供一个思路:
通过虚函数包装静态成员方法
代码实现:
#include<iostream>
using namespace std;
class Person {
public:
int member;
static void __speak() { cout << "I'm a person." << endl; }
// 虚函数包装
virtual void speak() { __speak(); }
};
class Programmer : public Person {
public:
int member;
static void __speak() { cout << "Life is shot, I'm a programmer." << endl; }
// 虚函数包装
virtual void speak() { __speak(); }
};
void test(Person& glh) {
glh.speak();
// > "Life is shot, I'm a programmer."
}
int main() {
Programmer glh;
test(glh);
system("pause");
return 0;
}