面向对象的三大特性
- 一、封装性
- 1. 封装性的意义
- 1.1 表现事物
- 1.2 权限控制
- 1.3 成员属性设置为私有
- 2. 封装性的衍生知识
- 2.1 struct和class区别
- 2.2 友元
- 2.2.1 全局函数做友元
- 2.2.2 类做友元
- 2.2.3 成员函数做友元
- 二、继承性
- 1. 继承的语法
- 2. 继承方式
- 3. 继承中的对象模型
- 3.1 说明
- 3.2 验证
- 4. 继承同名成员处理方式
- 4.1 同名属性
- 4.2 同名函数
- 4.2.1 参数相同
- 4.2.2 参数不同
- 4. 继承同名静态成员处理方式
- 5. 继承中构造和析构顺序
- 5.1 顺序说明
- 5.2 个人理解
- 6. 多继承语法
- 7. 菱形继承
- 7.1 菱形继承的概念
- 7.2 菱形继承的问题与解决
- 7.2.1 二义性
- 7.2.2 资源浪费
- 7.3 虚继承解决菱形继承问题的原理
- 8. Java和C++在继承性方面易混淆的区别
- 三、多态性
- 1. 多态的介绍
- 2. 多态的实现
- 2.1 失败的多态
- 2.2 成功的多态
- 2.3 个人理解
- 3. 纯虚函数和抽象类
- 3.1 纯虚函数语法
- 3.2 抽象类
- 4. 虚析构和纯虚析构
- 4.1 虚析构和纯虚析构语法
- 4.2 虚析构和纯虚析构异同
- 4.3 代码示例
- 4.4 个人理解
- 4.4.1 父类未使用虚析构函数
- 4.4.2 父类使用虚析构函数
C++认为万事万物都皆为对象,对象上有其属性和行为。例如:人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌…;车也可以作为对象,属性有轮胎、方向盘、车灯…,行为有载人、放音乐、放空调…。
具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类。
C++面向对象的三大特性为:封装、继承、多态。
一、封装性
1. 封装性的意义
封装是C++面向对象三大特性之一。封装性的存在主要有两大意义:①将属性和行为作为一个整体,表现生活中的事物;②将属性和行为加以权限控制。
1.1 表现事物
C++中封装性的声明语法如下:
class 类名{ 访问权限: 属性 / 行为 }
举例:设计圆类,求圆的周长
//圆周率 const double PI = 3.14; //1、封装的意义 //将属性和行为作为一个整体,用来表现生活中的事物 //封装一个圆类,求圆的周长 //class代表设计一个类,后面跟着的是类名 class Circle {public: //访问权限 公共的权限 //属性 int m_r;//半径 //行为 //获取到圆的周长 double calculateZC() {//2 * pi * r //获取圆的周长 return 2 * PI * m_r; } }; int main() {//通过圆类,创建圆的对象 // c1就是一个具体的圆 Circle c1; c1.m_r = 10; //给圆对象的半径 进行赋值操作 //2 * pi * 10 = = 62.8 cout << "圆的周长为: " << c1.calculateZC() << endl; system("pause"); return 0; }
1.2 权限控制
类在设计时,可以把属性和行为放在不同的权限下,加以控制。访问权限有三种:public(公共权限 )、protected(保护权限)、private(私有权限)。三种权限的控制规则如下:
- 公共权限public: 类内可以访问 类外可以访问
- 保护权限protected:类内可以访问,类外除子类不可以访问
- 私有权限private: 类内可以访问,类外不可以访问
举例:
//三种权限 //公共权限 public 类内可以访问 类外可以访问 //保护权限 protected 类内可以访问 类外不可以访问 //私有权限 private 类内可以访问 类外不可以访问 class Person {//姓名 公共权限 public: string m_Name; //汽车 保护权限 protected: string m_Car; //银行卡密码 私有权限 private: int m_Password; public: void func() {m_Name = "张三"; m_Car = "拖拉机"; m_Password = 123456; } }; int main() {Person p; p.m_Name = "李四"; //p.m_Car = "奔驰"; //保护权限类外访问不到 //p.m_Password = 123; //私有权限类外访问不到 system("pause"); return 0; }
1.3 成员属性设置为私有
我们设计类时,可以将所有成员属性设置为私有,可以自己控制读写权限。对于写权限,我们可以检测数据的有效性。
class Person {public: //姓名设置可读可写 void setName(string name) {m_Name = name; } string getName() {return m_Name; } //获取年龄 int getAge() {return m_Age; } //设置年龄 void setAge(int age) {if (age < 0 || age > 150) {cout << "你个老妖精!" << endl; return; } m_Age = age; } //情人设置为只写 void setLover(string lover) {m_Lover = lover; } private: string m_Name; //可读可写 姓名 int m_Age; //只读 年龄 string m_Lover; //只写 情人 }; int main() {Person p; //姓名设置 p.setName("张三"); cout << "姓名: " << p.getName() << endl; //年龄设置 p.setAge(50); cout << "年龄: " << p.getAge() << endl; //情人设置 p.setLover("苍井"); //cout << "情人: " << p.m_Lover << endl; //只写属性,不可以读取 system("pause"); return 0; }
2. 封装性的衍生知识
2.1 struct和class区别
在C++中,struct和class唯一的区别就在于默认的访问权限不同:struct默认权限为公共,class默认权限为私有。
2.2 友元
生活中你的家有客厅(Public),有你的卧室(Private)。客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去。但是呢,你也可以允许你的好闺蜜好基友进去。
在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术。换句话说,友元的目的就是让一个函数或者类 访问另一个类中私有成员。友元的关键字为friend。
友元的一共有三种实现:① 全局函数做友元 ②类做友元 ③成员函数做友元。
2.2.1 全局函数做友元
class Building {//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容 friend void goodGay(Building * building); public: Building() {this->m_SittingRoom = "客厅"; this->m_BedRoom = "卧室"; } public: string m_SittingRoom; //客厅 private: string m_BedRoom; //卧室 }; void goodGay(Building * building) {cout << "好基友正在访问: " << building->m_SittingRoom << endl; cout << "好基友正在访问: " << building->m_BedRoom << endl; } void test01() {Building b; goodGay(&b); } int main(){test01(); system("pause"); return 0; }
2.2.2 类做友元
class Building; class goodGay {public: goodGay(); void visit(); private: Building *building; }; class Building {//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容 friend class goodGay; public: Building(); public: string m_SittingRoom; //客厅 private: string m_BedRoom;//卧室 }; Building::Building() {this->m_SittingRoom = "客厅"; this->m_BedRoom = "卧室"; } goodGay::goodGay() {building = new Building; } void goodGay::visit() {cout << "好基友正在访问" << building->m_SittingRoom << endl; cout << "好基友正在访问" << building->m_BedRoom << endl; } void test01() {goodGay gg; gg.visit(); } int main(){test01(); system("pause"); return 0; }
2.2.3 成员函数做友元
class Building; class goodGay {public: goodGay(); void visit(); //只让visit函数作为Building的好朋友,可以发访问Building中私有内容 void visit2(); private: Building *building; }; class Building {//告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容 friend void goodGay::visit(); public: Building(); public: string m_SittingRoom; //客厅 private: string m_BedRoom;//卧室 }; Building::Building() {this->m_SittingRoom = "客厅"; this->m_BedRoom = "卧室"; } goodGay::goodGay() {building = new Building; } void goodGay::visit() {cout << "好基友正在访问" << building->m_SittingRoom << endl; cout << "好基友正在访问" << building->m_BedRoom << endl; } void goodGay::visit2() {cout << "好基友正在访问" << building->m_SittingRoom << endl; //cout << "好基友正在访问" << building->m_BedRoom << endl; } void test01() {goodGay gg; gg.visit(); } int main(){ test01(); system("pause"); return 0; }
二、继承性
继承是面向对象三大特性之一。有些类与类之间存在特殊的关系,例如下图:
我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。这个时候我们就可以考虑利用继承的技术,减少重复代码。
1. 继承的语法
class A : public B;
上述代码中,A类称为子类或派生类;B类称为父类或基类。
派生类中的成员,包含两大部分: 一类是从基类继承过来的,一类是自己增加的成员。
从基类继承过过来的表现其共性,而新增的成员体现了其个性。
2. 继承方式
继承方式一共有三种,分别是公共继承、保护继承、私有继承。三种方式的区别如下图所示:
注意,这里的不可访问只是因为权限设置无法直接调用私有成员(包括属性和方法)进行访问,但是实际上子类也继承了父类的私有成员,可以通过其他方式进行访问。这点后面会陆续说到。
class Base1 {public: int m_A; protected: int m_B; private: int m_C; }; //公共继承 class Son1 :public Base1 {public: void func() {m_A; //可访问 public权限 m_B; //可访问 protected权限 //m_C; //不可访问 } }; void myClass() {Son1 s1; s1.m_A; //其他类只能访问到公共权限 } //保护继承 class Base2 {public: int m_A; protected: int m_B; private: int m_C; }; class Son2:protected Base2 {public: void func() {m_A; //可访问 protected权限 m_B; //可访问 protected权限 //m_C; //不可访问 } }; void myClass2() {Son2 s; //s.m_A; //不可访问 } //私有继承 class Base3 {public: int m_A; protected: int m_B; private: int m_C; }; class Son3:private Base3 {public: void func() {m_A; //可访问 private权限 m_B; //可访问 private权限 //m_C; //不可访问 } }; class GrandSon3 :public Son3 {public: void func() {//Son3是私有继承,所以继承Son3的属性在GrandSon3中都无法访问到 //m_A; //m_B; //m_C; } };
3. 继承中的对象模型
3.1 说明
子类中私有成员只是被隐藏了,无法直接访问到,但是还是会继承下去。
3.2 验证
class Base {public: int m_A; protected: int m_B; private: int m_C; //私有成员只是被隐藏了,但是还是会继承下去 }; //公共继承 class Son :public Base {public: int m_D; }; void test01() {cout << "sizeof Son = " << sizeof(Son) << endl; } int main() {test01(); system("pause"); return 0; }
利用【开发人员命令提示符工具】查看:打开工具窗口后,定位到当前C++文件的盘符,输入:
cl /d1 reportSingleClassLayout查看的类名 所属文件名
即可查看所需要查看类的内部数据,效果图如下:
4. 继承同名成员处理方式
4.1 同名属性
当子类定义了与父类同名的属性以后,子类会将父类的同名属性进行隐藏。此时直接访问同名属性时,调用的是子类定义的同名属性。
在子类内部,可以通过属性名或子类::属性名的方式对只属于子类的同名属性进行调用;可以通过父类::属性名的方式对从父类继承下来的属于子类的同名属性进行调用。
在子类外部,可以可以通过子类对象.属性名或子类对象.子类::属性名的方式对只属于子类的同名属性进行调用;可以通过子类对象.父类::属性名的方式对从父类继承下来的属于子类的同名属性进行调用。
class Base {public: int m_A = 10; }; //公共继承 class Son :public Base {public: int m_A = 100; public: void print_info() {//子类内 cout << Base::m_A << endl; cout << Son::m_A << endl; cout << m_A << endl; } }; void test01() {Son s; s.print_info(); //子类外 cout << s.Base::m_A << endl; cout << s.Son::m_A << endl; cout << s.m_A << endl; } int main() {test01(); system("pause"); return 0; }
4.2 同名函数
当子类定义了与父类同名的函数以后,子类会将父类的同名函数性进行隐藏。细分来说,可以再分为两种情况进行讨论:① 父类中,与子类的同名函数参数和子类相同;②父类中,与子类同名的函数参数和子类不同。
4.2.1 参数相同
在子类内部,可以通过函数名或子类::函数名的方式对只属于子类的同名函数进行调用;可以通过父类::函数性名的方式对从父类继承下来的属于子类的同名函数进行调用。
在子类外部,可以通过子类对象.函数名或子类对象.子类::函数名的方式对只属于子类的同名函数进行调用;可以通过子类对象.父类::属性名的方式对从父类继承下来的属于子类的同名函数进行调用。
class Base {public: void print_str() {cout << "str1" << endl; } }; //公共继承 class Son :public Base {public: void print_str() {cout << "str2" << endl; } void print_info() {//子类内 Base::print_str(); Son::print_str(); print_str(); } }; void test01() {Son s; s.print_info(); cout << "-----------------------"<< endl; //子类外 s.Base::print_str(); s.Son::print_str(); s.print_str(); } int main() {test01(); system("pause"); return 0; }
4.2.2 参数不同
在子类内部,由于子类的同名函数把父类所有名称相同的函数,都进行了隐藏,因此,无法通过函数名(参数)的方式对子类的继承的不同参数的同名函数进行调用;可以通过父类::函数名(参数)的方式对从父类继承下来的属于子类的同名函数进行调用。
在子类外部,也同样无法通过子类对象.函数名(参数)的方式对子类的继承的不同参数的同名函数进行调用,只能通过子类对象.父类::函数名(参数)的方式进行调用。
class Base {public: void print_str() {cout << "str1" << endl; } void print_str(string& str) {cout << str << endl; } }; //公共继承 class Base {public: void print_str() {cout << "str1" << endl; } void print_str(string& str) {cout << str << endl; } }; //公共继承 class Son :public Base {public: void print_str() {cout << "str2" << endl; } void print_info() {string name = "jerry"; //子类内 //失败 //print_str(name); Base::print_str(name); } }; void test01() {Son s; s.print_info(); cout << "-----------------------" << endl; //子类外 string str = "hello"; //失败 //s.print_str(str); s.Base::print_str(str); } int main() {test01(); system("pause"); return 0; }
4. 继承同名静态成员处理方式
静态成员和非静态成员出现同名,处理方式一致,只不过有两种访问的方式:通过对象和通过类名。
class Base {public: static void func() {cout << "Base - static void func()" << endl; } static void func(int a) {cout << "Base - static void func(int a)" << endl; } static int m_A; }; int Base::m_A = 100; class Son : public Base {public: static void func() {cout << "Son - static void func()" << endl; } static int m_A; static void call_static() {cout << "Son 下 m_A = " << m_A << endl; cout << "Son 下 m_A = " << Son::m_A << endl; cout << "Base 下 m_A = " << Base::m_A << endl; Son::func(); Son::Son::func(); Son::Base::func(); Son::Base::func(100); } }; int Son::m_A = 200; //同名成员属性 void test01() {//通过对象访问 cout << "通过对象访问: " << endl; Son s; cout << "Son 下 m_A = " << s.m_A << endl; cout << "Son 下 m_A = " << s.Son::m_A << endl; cout << "Base 下 m_A = " << s.Base::m_A << endl; //通过类名访问 cout << "通过类名访问: " << endl; cout << "Son 下 m_A = " << Son::m_A << endl; cout << "Son 下 m_A = " << Son::Son::m_A << endl; cout << "Base 下 m_A = " << Son::Base::m_A << endl; } //同名成员函数 void test02() {//通过对象访问 cout << "通过对象访问: " << endl; Son s; s.func(); s.Son::func(); s.Base::func(); cout << "通过类名访问: " << endl; Son::func(); Son::Son::func(); Son::Base::func(); //出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问 Son::Base::func(100); } //子类中访问 void test3() {Son::call_static(); } int main() {test01(); test02(); test3(); system("pause"); return 0; }
5. 继承中构造和析构顺序
5.1 顺序说明
子类继承父类后,当创建子类对象,也会调用父类的构造函数。构造函数的调用顺序为:先调用父类构造函数,再调用子类构造函数。析构函数的调用顺序为:先调用子类析构函数,再调用父类析构函数。
class Base {public: Base() {cout << "Base构造函数!" << endl; } ~Base() {cout << "Base析构函数!" << endl; } }; class Son : public Base {public: Son() {cout << "Son构造函数!" << endl; } ~Son() {cout << "Son析构函数!" << endl; } }; void test01() {//继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反 Son s; } int main() {test01(); system("pause"); return 0; }
5.2 个人理解
c++创建一个子类对象时会调用父类的构造函数,但不会创建另外一个父类对象,只是初始化子类中属于父类的成员。
创建一个对象的时候,发生了两件事情,一是分配对象所需的内存,二是调用构造函数进行初始化。子类对象包含从父类对象继承过来的成员,实现上来说,一般也是子类的内存区域中有一部分就是父类的内存区域。调用父类构造函数的时候,这块父类对象的内存区域就被初始化了。为了避免未初始化的问题,语法强制子类调用父类构造函数。
6. 多继承语法
C++允许一个类继承多个类,语法如下:
class 子类 :继承方式 父类1 , 继承方式 父类2...
但是,多继承可能会引发父类中有同名成员出现,此时子类在使用时,需要加作用域区分。C++实际开发中不建议用多继承。
class Base1 {public: Base1() {m_A = 100; } public: int m_A; }; class Base2 {public: Base2() {m_A = 200; //开始是m_B 不会出问题,但是改为mA就会出现不明确 } public: int m_A; }; //语法:class 子类:继承方式 父类1 ,继承方式 父类2 class Son : public Base2, public Base1 {public: Son() {m_C = 300; m_D = 400; } public: int m_C; int m_D; }; //多继承容易产生成员同名的情况 //通过使用类名作用域可以区分调用哪一个基类的成员 void test01() {Son s; cout << "sizeof Son = " << sizeof(s) << endl; cout << s.Base1::m_A << endl; cout << s.Base2::m_A << endl; } int main() {test01(); system("pause"); return 0; }
7. 菱形继承
7.1 菱形继承的概念
菱形继承:两个派生类继承同一个基类,某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石继承。
7.2 菱形继承的问题与解决
7.2.1 二义性
- 问题:羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
- 解决方式:通过使用作用域对数据进行区分。
class Animal {public: int m_Age; }; class Sheep : public Animal {}; class Tuo : public Animal {}; class SheepTuo : public Sheep, public Tuo {}; void test01() {SheepTuo st; st.Sheep::m_Age = 100; st.Tuo::m_Age = 200; cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; } int main() {test01(); system("pause"); return 0; }
7.2.2 资源浪费
- 问题:草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
- 解决方式:通过使用虚继承的方式可以解决此问题。让羊和驼都以虚继承的方式来继承动物类,羊驼以正常的继承方式继承羊类和驼类。此时,动物类成为虚基类。虚继承同样可以解决二义性的问题。
class Animal {public: int m_Age; }; //继承前加virtual关键字后,变为虚继承 //此时公共的父类Animal称为虚基类 class Sheep : virtual public Animal {}; class Tuo : virtual public Animal {}; class SheepTuo : public Sheep, public Tuo {}; void test01() {SheepTuo st; st.Sheep::m_Age = 100; st.Tuo::m_Age = 200; cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; cout << "st.m_Age = " << st.m_Age << endl; } int main() {test01(); system("pause"); return 0; }
7.3 虚继承解决菱形继承问题的原理
采用正常的继承方式,羊驼类内的结构如图,可以看到羊驼类中有两份m_Age数据,一份来源于羊类,一份来源于驼类。
采用虚继承的方式,羊驼类内的结构如图,可以发现,此时羊驼类内部只有一个m_Age数据,从羊类和驼类中继承的都是vbptr。vbptr是Vitrual Base Pointer,即虚基类指针的意思,这个指针会指向羊驼类中各自的vbtable。vbtable是Virtual Base Table,即虚基类表格的意思,该表格记录了偏移量。vbptr所在的位置+vbtable中的偏移量=实际存放值的位置,下图中羊类的vbptr位置为4,羊类的vbtable记录的偏移量为8,0+8=8,8就是m_Age的位置。
可以看出,此时羊驼类中只有一个m_Age。无论通过何种方式访问m_Age,都是在访问同一个数据。由此,也就解决了菱形继承带来的二义性和资源浪费的问题。
8. Java和C++在继承性方面易混淆的区别
Java C++ Java中只有一种继承方式 C++中有三种继承方式 Java中子类会重写父类中同名同参数的方法,而与父类中同名不同参数的方法是重载的关系 C++中会隐藏父类中所有同名方法(不管参数相同不相同) 三、多态性
多态是C++面向对象三大特性之一。
1. 多态的介绍
C++中的多态分为两类:
- 静态多态: 函数重载和运算符重载属于静态多态,复用函数名。
- 动态多态: 派生类和虚函数实现运行时多态。
静态多态和动态多态区别:
- 静态多态的函数地址早绑定:编译阶段确定函数地址。
- 动态多态的函数地址晚绑定:运行阶段确定函数地址。
动态多态的本质是父类指针或引用指向子类对象。本部分所介绍的多态,指的都是动态多态。因此,为了方便起见,下文只要说到多态的地方,都代表动态多态。
2. 多态的实现
如果想要实现多态,必须要满足以下两个条件:
- 两个类有继承关系
- 子类重写父类中的虚函数
重写和重载是两种概念:
- 重写:函数返回值类型、函数名、参数列表完全一致称为重写
- 重载:函数的参数名相同,而参数列表不同
2.1 失败的多态
class Animal {public: void speak() {cout << "动物在说话" << endl; } }; class Cat :public Animal {public: void speak() {cout << "小猫在说话" << endl; } }; class Dog :public Animal {public: void speak() {cout << "小狗在说话" << endl; } }; void DoSpeak(Animal& animal) {animal.speak(); } void test01() {Cat cat; DoSpeak(cat); Dog dog; DoSpeak(dog); } int main() {test01(); system("pause"); return 0; }
此时,屏幕输出的都是【动物在说话】,并没有真正的实现多态。这是因为DoSpeak属于地址早绑定,该函数在编译时已经确定了函数的地址。
2.2 成功的多态
class Animal {public: virtual void speak() {cout << "动物在说话" << endl; } }; class Cat :public Animal {public: void speak()//也可以加上virtual关键字,不加也没事 {cout << "小猫在说话" << endl; } }; class Dog :public Animal {public: void speak()//也可以加上virtual关键字,不加也没事 {cout << "小狗在说话" << endl; } }; void DoSpeak(Animal& animal) {animal.speak(); } void test01() {Cat cat; DoSpeak(cat); Dog dog; DoSpeak(dog); } int main() {test01(); system("pause"); return 0; }
此时,屏幕输出的是【小猫在说话】和【小狗在说话】,实现了真正的实现多态。那么为什么函数定义为虚函数就可以实现多态性呢?
- 当函数只是普通的函数时,类中并不存储该函数。
- 当函数是虚函数时,类中会存储一个vfptr(Virtual Function Pointer),即虚函数(表)指针,该指针会指向vftable(Virtual Function Table)虚函数表,表中会记录虚函数的地址&类名::函数(在上述例子中就是&Animal::speak)。
- 子类继承该类但并未重写虚方法时,子类也会有一个和父类完全相同的vfptr和vftable。
- 子类继承该类并重写虚方法以后,子类会将虚函数表内部存放的地址进行替换,替换成子类的虚函数地址(在上述例子中就是&Cat::speak)。
- 当父类的指针或引用指向子类对象的时候,就会去子类对象的虚函数表中找对应的函数,也就发生了多态。
2.3 个人理解
- 发生多态,最核心的原因就是地址的晚绑定,即在运行阶段才能真正的确定函数的地址。
- 在未采用虚函数时,类中不会存放函数的地址,函数的地址是类外的某个具体内存。此时函数地址的确认不依赖于具体的对象,因此直接能够在编译阶段完成函数地址的确认,所以无法发生多态。
- 采用虚函数以后,类中会存放函数的地址指针,地址指针指向虚函数表,虚函数表指向真正运行的函数地址。此时,函数地址的确认要依赖于具体的对象,所以无法在编译阶段完成函数地址的确认,必须要在运行时,根据具体指向的对象找到函数的地址,进行调用,因此才能实现多态。
3. 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
3.1 纯虚函数语法
virtual 返回值类型 函数名 (参数列表)= 0 ;
3.2 抽象类
当类中有了纯虚函数,这个类也称为抽象类。抽象类具有以下特点:
- 无法实例化对象。
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
class Base {public: //纯虚函数 //类中只要有一个纯虚函数就称为抽象类 //抽象类无法实例化对象 //子类必须重写父类中的纯虚函数,否则也属于抽象类 virtual void func() = 0; }; class Son :public Base {public: virtual void func() {cout << "func调用" << endl; }; }; void test01() {Base * base = NULL; //base = new Base; // 错误,抽象类无法实例化对象 base = new Son; base->func(); delete base;//记得销毁 } int main() {test01(); system("pause"); return 0; }
4. 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,此时,就需要将父类中的析构函数改为虚析构或者纯虚析构。
4.1 虚析构和纯虚析构语法
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0; 类名::~类名(){}//纯虚析构需要实现
4.2 虚析构和纯虚析构异同
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象。
- 都需要有具体的函数实现。
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象。
4.3 代码示例
class Animal {public: Animal() {cout << "Animal 构造函数调用!" << endl; } virtual void Speak() = 0; //析构函数加上virtual关键字,变成虚析构函数 //virtual ~Animal() //{// cout << "Animal虚析构函数调用!" << endl; //} virtual ~Animal() = 0; }; Animal::~Animal() {cout << "Animal 纯虚析构函数调用!" << endl; } //和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。 class Cat : public Animal {public: Cat(string name) {cout << "Cat构造函数调用!" << endl; m_Name = new string(name); } virtual void Speak() {cout << *m_Name << "小猫在说话!" << endl; } ~Cat() {cout << "Cat析构函数调用!" << endl; if (this->m_Name != NULL) {delete m_Name; m_Name = NULL; } } public: string *m_Name; }; void test01() {Animal *animal = new Cat("Tom"); animal->Speak(); //通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏 //怎么解决?给基类增加一个虚析构函数 //虚析构函数就是用来解决通过父类指针释放子类对象 delete animal; } int main() {test01(); system("pause"); return 0; }
4.4 个人理解
4.4.1 父类未使用虚析构函数
如果在父类没有使用虚析构函数的时候对父类指针进行了释放,那么会调用父类的析构函数(早绑定),此时子类的空间会被释放(地址指向的就是子类的地址),但不会调用子类的析构函数。在释放子类的空间时,指针自身并不关心它所指向的对象有哪些成员变量,因此子类的成员变量也会被一并释放。然而,如果这个子类有任何动态分配的内存(例如使用 new 创建的数组或者其他对象),那么这些动态分配的内存会在子类对象被释放时不会被自动回收,最终会造成内存泄漏。
4.4.2 父类使用虚析构函数
如果在父类使用虚析构函数的时候对父类指针进行了释放,那么会调用子类的析构函数(晚绑定),此时子类的空间会被释放,如果这个子类有任何动态分配的内存,我们可以在子类空间中人为进行释放来避免内存泄露。