面向对象程序设计
派生与继承
继承的方式
派生类(子类)是从基类(父类)继承而来的类,派生类将继承基类的属性;
class Entity { public: float X, Y; void Move(float ax, float ay) { X += ax; Y += ay; } };
公有继承:基类中的公有成员继承为公有成员,保护成员继承为保护成员。
class Player :public Entity //派生类列表,公有继承 { public: const char* name; void PrintName() { std::cout << name << std::endl; } };
保护继承:基类中的公有和保护成员都继承为保护成员。
class Player :protected Entity
私有继承:基类中的公有和保护成员都继承为私有成员。
class Player :private Entity
基类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到
继承中构造和析构顺序
- 子类继承父类后,当创建子类对象,也会调用父类的构造函数。
- 继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
继承同名成员处理方式
继承同名非静态成员:
子类对象可以直接访问到子类中同名成员。
Player p; p.m_X; p.function();
子类对象加作用域可以访问到父类同名成员。
p.Entity::m_X; P.Entity::function();
当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数。
继承同名静态成员:
通过对象访问时与非静态成员相同.
通过类名访问时
访问子类中同名成员
Player::m_X; Player::function();
访问父类中同名成员
Player::Entity::m_X; Player::Entity::function();
多继承
C++允许一个类继承多个类,但不建议使用
语法:
class 子类 :继承方式 父类1 , 继承方式 父类2...
class Player : public Base1, public Base2 {};
多继承可能会引发父类中有同名成员出现,需要加作用域区分
Player p; p.Base1::m_X; p.Base2::m_X;
菱形继承
概念:两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石继承。
class Animal { public: int m_Age; }; class Tiger :public Animal {}; class Lion :public Animal{}; class Liger :public Tiger, public Lion{}; int main() { Liger l; l.Tiger::m_Age = 10; l.Lion::m_Age = 12; std::cout << l.Tiger::m_Age << std::endl; //10 std::cout << l.Lion::m_Age << std::endl; //12 }
菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
利用虚继承可以解决菱形继承问题,虚继承后原本相同的数据都变为一份,可直接通过对象访问,也可通过虚基类指针访问;
class Tiger : virtual public Animal {}; class Lion : virtual public Animal{}; int main() { Liger l; l.Tiger::m_Age = 10; l.Lion::m_Age = 12; //相当于把m_Age修改为12 std::cout << l.Tiger::m_Age << std::endl; //12使用虚基类指针访问继承的数据 std::cout << l.Lion::m_Age << std::endl; //12 }
虚继承的本质:
class Liger size(12): +--- 0 | +--- (base class Tiger) //基类 0 | | {vbptr} //虚基类指针virtual base pointer,指向虚基类表 | +--- 4 | +--- (base class Lion) 4 | | {vbptr} | +--- +--- +--- (virtual base Animal) 8 | m_Age //虚继承的数据 +--- Liger::$vbtable@Tiger@: //vbtable虚基类表 0 | 0 1 | 8 (Ligerd(Tiger+0)Animal) //偏移量 Liger::$vbtable@Lion@: 0 | 0 1 | 4 (Ligerd(Lion+0)Animal) //偏移量 vbi: class offset o.vbptr o.vbte fVtorDisp Animal 8 0 4 0
多态
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
虚函数
虚函数的基本概念
虚函数引入了动态联编,通过虚函数表(vtbl)实现编译。虚函数允许我们在子类中重写方法。当类中存在虚函数,对象中就会多一个指针,该指针即是虚函数表指针(vptr)。
当父类有虚函数时,子类一定有虚函数,也就是有虚函数表指针,因为子类继承父类的函数继承的是调用权,而不是内存大小。
虚函数关键字:virtual,override。
虚函数表(v表):
- 虚函数表包含基类中所有虚函数的映射。
- 虚函数表可以在运行时将虚函数映射到正确的覆写函数。
class Entity { public: virtual std::string GetName() { return "Entity"; } //虚函数 }; class Player :public Entity { private: std::string m_name; public: Player(const std::string& name) : m_name(name) {} std::string GetName() override { return m_name; } //覆写函数 };
虚函数的额外运行成本:
- 需要额外的内存储存虚函数表,基类中需要一个成员指针指向虚函数表。
- 每次调用虚函数需要查找虚函数表,来确定映射到哪个函数,会造成额外的开销。
虚函数的原理
子类继承父类时会使用一个虚函数表存放内容与父类相同的虚函数表,若是子类覆写虚函数,则会用新函数的地址覆盖掉虚函数表中对应的虚函数指针。
C语言中调用函数会进行call指令,跳转到函数的地址,这是静态绑定。虚函数则是使用动态绑定,当调用虚函数时,会先通过虚函数表指针找到虚函数表,再通过虚函数表中的虚函数指针找到要调用的虚函数。
动态绑定的条件:
- 通过指针调用。
- 指针向上转型,即父类指针指向子类对象。
- 调用的是虚函数。
class Entity size(4): +--- 0 | {vfptr} //虚函数表指针 +--- Entity::$vftable@: //虚函数表 | &Entity_meta | 0 0 | &Entity::GetName //基类中的虚函数地址
class Player size(28): +--- 0 | +--- (base class Entity) 0 | | {vfptr} //虚函数表指针 | +--- 4 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ m_name +--- Player::$vftable@: | &Player_meta | 0 0 | &Entity::GetName //子类拥有覆写函数前虚函数表储存父类的虚函数
class Player size(28): +--- 0 | +--- (base class Entity) 0 | | {vfptr} //虚函数表指针 | +--- 4 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ m_name +--- Player::$vftable@: | &Player_meta | 0 0 | &Player::GetName //子类拥有覆写函数后覆写函数的地址会覆盖父类虚函数的地址
使用C语言模拟虚函数的调用:
纯虚函数
纯虚函数语法:
virtual 返回值类型 函数名 (参数列表)= 0 ;
纯虚函数允许在基类定义一个没有实现的函数,然后强制派生类实现该函数。
创建一个类,只有未实现的方法组成,然后强制派生类去实现它们,这是C++中的接口。
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Printable //接口 { public: virtual std::string GetClassName() = 0; //构造纯虚函数 }; class Entity : public Printable { public: virtual std::string GetName() { return "Entity"; } std::string GetClassName() override { return "Entity"; } }; class Player :public Entity { private: std::string m_name; public: Player(const std::string& name) :m_name(name) {} std::string GetName() override { return m_name; } std::string GetClassName() override { return "Player"; } }; void PrintName(Entity* entity) { std::cout << entity->GetClassName() << std::endl; } int main() { Entity* e = new Entity(); Player* p = new Player("wsdsm"); PrintName(e); PrintName(p); delete e; delete p; }
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,可将父类中的析构函数改为虚析构或者纯虚析构来解决。
析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0; 类名::~类名(){}
class Entity { public: virtual std::string GetName() { return "Entity"; } //虚函数 virtual ~Entity() = 0; //声明一个纯虚函数 }; Entity::~Entity(){} //纯虚函数必须具体实现函数 class Player :public Entity { private: std::string* m_name; public: Player(const std::string& name) { m_name = new std::string(name); //在堆区创建内存 } ~Player() //子类重写虚函数 { if (m_name) { //若m_name不为空,释放内存 delete m_name; m_name = NULL; } } std::string GetName() override { return *m_name; } //覆写函数 }; int main() { Entity* p = new Player("wsdsm"); std::cout << p->GetName() << std::endl; }
可见性
可见性修饰符:public,projected,private
- private:只有该类及其友元(friend修饰的类或函数)可以访问。
- projected:该类及其层次结构中的所有子类都可访问。
- public:可随意访问。
友元
关键字:friend
友元的目的:让一个函数或者类 访问另一个类中私有成员。
全局函数做友元
class Entity { public: int m_X; private: int m_Y; public: Entity() { this->m_X = 1; this->m_Y = 1; } //告诉编译器 Function全局函数,是Entity类的友元,可以访问类中的私有内容 friend void Function(Entity* e); }; void Function(Entity* e) { e->m_X = 2; e->m_Y = 2; }
类做友元
friend class Entity;
成员函数做友元
friend void Entity::Function();