4.6 继承
继承是面向对象三大特性之一
子类拥有父类的一些共性
利用继承的技术可以减少重复的代码
4.6.1 继承方式
语法: class 子类 : 继承方式 父类{}
子类 也被称为
派生类
父类 也被称为基类
示例:
// 继承
class Animal
{
public:
string name; // 动物名称
int age; // 动物年龄
};
class Dog
{
public:
string leg; // 狗腿
};
**总结:**
继承的好处:`可以减少重复代码`
派生类中的成员,包含两大部分
- 一类是从基类继承过来的,一类是自己的成员
- 从基类继承继承过来表现其共性,而新增的成员体现了个性。
4.6.2 继承的方式
继承的方式一共有三种
- 公共继承
- 保护继承
- 私有继承
4.6.3 继承中的对象模型
问题:从父类继承过来的成员,哪些属于子类对象中?
示例:
#include<iostream>
using namespace std;
#include <string>
// 继承中的对象模型
class Base
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son : public Base
{
public:
int m_D;
};
// 利用VS提供的开发人员命令提示工具查看对象模型
// 在文件路径下操作:cl /d1 reportSingleClassLayout class_name
void test01()
{
// 最终结果是 16
cout << "sizeof of Son = " << sizeof(Son) << endl;
// 结论:父类中所有的非静态的成员属性都会被子类继承下去
// 父类中私有成员属性 是被编译器给隐藏了,因此访问不到,但是确实被继承了
}
int main()
{
test01();
return 0;
}
结论: 父类中所有的非静态的成员属性都会被子类继承下去,父类中私有成员属性是被编译器给隐藏了,因此访问不到,但是确实被继承了
4.6.4 继承中的构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
问题: 父类和子类中的构造函数和析构函数顺序呢?
示例:
#include<iostream>
using namespace std;
#include <string>
// 继承中的对象模型
class Base
{
public:
Base()
{
cout << "Base 构造函数执行了" << endl;
}
~Base()
{
cout << "Base 析构函数执行了" << endl;
}
public:
int m_A;
};
class Son : public Base
{
public:
Son()
{
cout << "Son 构造函数执行了" << endl;
}
~Son()
{
cout << "Son 析构函数执行了" << endl;
}
};
void test01()
{
Son son;
}
int main()
{
test01();
return 0;
}
运行结果:
结论: 先构造父类,然后构造儿子,析构则是相反。
4.6.5 继承中同名成员处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或者父类中的同名数据呢?
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
示例:
#include<iostream>
using namespace std;
#include <string>
// 继承中同名成员处理
class Base
{
public:
Base()
{
m_A = 100;
}
void func()
{
cout << "父类中的函数调用" << endl;
}
void pr()
{
cout << "父类中的pr()函数调用" << endl;
}
public:
int m_A;
};
class Son : public Base
{
public:
Son()
{
m_A = 200;
}
void func()
{
cout << "子类中的函数调用" << endl;
}
public:
int m_A;
};
// 同名的成员属性
void test01()
{
Son son;
cout << "子类中 m_A = " << son.m_A << endl;
cout << "父类中 m_A = " << son.Base::m_A << endl;
}
// 同名的成员函数处理
void test02()
{
Son son;
son.func();
son.pr();
son.Base::func();
// 如果子类中出现和父类同名的成员函数,子类的同名成员会影藏掉父类中所有同名的成员函数:重载的也被影藏,仍然要添加作用域
}
int main()
{
test01();
test02();
return 0;
}
运行结果:
总结:
- 子类对象可以直接访问到子类中的同名成员
- 子类对象加作用域可以访问到父类中的同名成员,示例:
son.Base::func();
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中的同名成员函数,加作用域可以访问到父类中同名函数
4.6.6 继承同名静态成员处理方式
问题:继承中同名的静态成员在子类对象如何进行访问?
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
示例:
#include<iostream>
using namespace std;
#include <string>
// 继承中同名静态成员处理
class Base
{
public:
static void func()
{
cout << "父类中 func() 的调用" << endl;
}
public:
static int m_A;
};
// 静态成员初始化
int Base::m_A = 100;
class Son : public Base
{
public:
static void func()
{
cout << "子类中 func() 的调用" << endl;
}
public:
static int m_A;
};
int Son::m_A = 200;
// 同名的静态成员属性
void test01()
{
Son son;
cout << "通过对象访问:" << endl;
cout << "子类中 m_A = " << son.m_A << endl;
cout << "父类中 m_A = " << son.Base::m_A<< endl;
cout << "通过类名访问:" << endl;
cout << "子类中 m_A = " << Son::m_A << endl; // 此处的双冒号代表通过类名访问
cout << "父类中 m_A = " << Son::Base::m_A << endl; // 此处双冒号代表父类作用域下
}
// 同名的静态成员函数
void test02()
{
Son son;
cout << "通过对象访问:" << endl;
son.func();
son.Base::func();
cout << "通过类名访问:" << endl;
Son::func();
Son::Base::func();
}
int main()
{
test01();
test02();
return 0;
}
总结: 同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象和通过类),此外,如果函数重载,会隐藏父类中的同名函数,需要添加作用域才可以访问。
4.6.7 多继承语法
C++允许一个类继承多个类
语法:class 子类: 继承方式 父类1, 继承方式 父类二 ...{}
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议使用多继承
示例:
#include<iostream>
using namespace std;
#include <string>
class Base1
{
public:
int m_A = 100;
};
class Base2
{
public:
int m_A = 200;
};
class Son : public Base1, public Base2
{
public:
int m_B = 300;
};
void test01()
{
Son son;
cout << "子类中 m_B = " << son.m_B << endl;
cout << "父类Base1中 m_A = " << son.Base1::m_A<< endl;
cout << "父类Base2中 m_A = " << son.Base2::m_A << endl;
}
int main()
{
test01();
return 0;
}
运行结果:
总结: 多继承中如果父类中出现了同名的情况,访问需要使用作用域区别
4.6.8 菱形继承
概念:
两个派生类继承同一个基类
又有某个类同时继承两个派生类
这种继承被称为菱形继承,或者钻石继承
典型的菱形继承案例:
菱形继承问题:
- 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据就会产生二义性
- 草泥马继承自动物的数据继承了两份,但是这个数据只需要一份就可以。
问题解决:使用虚继承
示例:
#include<iostream>
using namespace std;
#include <string>
// 动物类
class Animal
{
public:
public:
int age = 100;
};
// 利用虚继承 解决菱形继承的问题
// 继承之前 加上关键字 virtual 变为虚继承
// Animal 被称为 虚基类
// 羊类
class Sheep: virtual public Animal
{
};
// 驼类
class Camel : virtual public Animal
{
};
// 羊驼类 alpaca
class Alpaca: public Sheep, public Camel
{
};
void test01()
{
Alpaca alpaca;
alpaca.Sheep::age = 18;
alpaca.Camel::age = 20;
// 当出现菱形继承的时候,两个父类拥有相同的数据,需要加以作用域区分
cout << "alpaca.Sheep::age = " << alpaca.Sheep::age << endl;
cout << "alpaca.Camel::age = " << alpaca.Camel::age << endl;
cout << "alpaca.age = " << alpaca.age << endl;
cout << "sizeof Sheep = " << sizeof(Sheep) << endl;
cout << "sizeof Camel = " << sizeof(Camel) << endl;
cout << "sizeof Alpaca = " << sizeof(Alpaca) << endl;
}
int main()
{
test01();
return 0;
}
运行结果:
4.7 多态
4.7.1 多态的基本概念
多态是C++面向对象三大特性之一
多态分为两类
- 静态多态:函数重载,和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
下面通过案例进行讲解多态:
#include<iostream>
using namespace std;
#include <string>
// 动物类
class Animal
{
public:
// 关键字 virtual 使函数地址在运行时绑定
virtual void speak()
{
cout << "父类对应为虚函数:动物在说话" << endl;
}
void bark()
{
cout << "父类对应为普通:动物在咬" << endl;
}
};
class Cat: public Animal
{
public:
void speak()
{
cout << "父类对应为虚函数:小猫在说话" << endl;
}
void bark()
{
cout << "父类对应为普通:函数小猫在咬" << endl;
}
};
// 执行说话的函数:父类对应函数为虚函数,
// 函数地址晚绑定, 在运行阶段确定函数地址
void doSpeak(Animal &animal) // Animal& animal = cat
{
animal.speak();
}
// 执行说话的函数:父类对应函数为普通函数
// 地址早帮定,在编译阶段确定函数地址
void doBark(Animal &animal)
{
animal.bark();
}
void test01()
{
Cat cat;
doSpeak(cat);
doBark(cat);
}
int main()
{
test01();
return 0;
}
执行结果:
动态多态满足条件
- 有继承关系
- 子类重写父类的虚函数
动态多态的使用:
父类的指针或者引用 执行子类对象。
Animal& animal = cat;
animal.speak()
重写: 函数返回值类型、函数名、参数列表完全一致称为重写
虚函数底层:
4.7.2 多态案例–计算器
案例描述:
分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
传统方法示例:
// 普通的写法
class Calculator
{
public:
int a;
int b;
public:
int getResult(string oper)
{
if (oper == "+")
{
return a + b;
}
else if (oper == "-")
{
return a - b;
}
else if (oper == "*")
{
return a * b;
}
else if (oper == "/")
{
return a / b;
}
// 如果想扩展新的功能,需要修改源码
// 在真实的开发中,提供开闭原则
// 开闭原则:对扩展进行开放,对修改进行关闭
}
};
void test01()
{
// 创建一个Calculate对象
Calculator c;
c.a = 10;
c.b = 20;
cout << c.a << "+" << c.b << " = " << c.getResult("+") << endl;
cout << c.a << "-" << c.b << " = " << c.getResult("-") << endl;
cout << c.a << "*" << c.b << " = " << c.getResult("*") << endl;
cout << c.a << "/" << c.b << " = " << c.getResult("/") << endl;
}
int main()
{
test01();
return 0;
}
运行结果:
利用多态的示例:
// 利用多态实现计算器
class AbstractCaluclator
{
public:
virtual int getResult()
{
return 0;
}
int a;
int b;
};
// 设计一个加法计算器类
class AddCalculator : public AbstractCaluclator
{
public:
int getResult()
{
return a + b;
}
};
// 设计一个减法计算器类
class SubCalculator : public AbstractCaluclator
{
public:
int getResult()
{
return a - b;
}
};
// 设计一个乘法计算器类
class MulCalculator : public AbstractCaluclator
{
public:
int getResult()
{
return a * b;
}
};
// 设计一个除法计算器类
class DivideCalculator : public AbstractCaluclator
{
public:
int getResult()
{
return a / b;
}
};
void test02()
{
// 加法
AbstractCaluclator* c = new AddCalculator();
c->a = 10;
c->b = 20;
cout << c->a << "+" <<c->b << " = " << c->getResult() << endl;
// 使用完毕,记得销毁
delete c; // 销毁的只是指正指向的堆区的数据
// 减法
c = new SubCalculator;
c->a = 10;
c->b = 20;
cout << c->a << "-" << c->b << " = " << c->getResult() << endl;
// 使用完毕,记得销毁
delete c; // 销毁的只是指正指向的堆区的数据
// 乘法
c = new MulCalculator;
c->a = 10;
c->b = 20;
cout << c->a << "*" << c->b << " = " << c->getResult() << endl;
// 使用完毕,记得销毁
delete c; // 销毁的只是指正指向的堆区的数据
}
int main()
{
test02();
return 0;
}
运行结果:
利用多态的好处总结:
- 组织结构清晰
- 可读性强
- 对于前期和后期的可扩展性以及可维护性高
4.7.3 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 ( 参数列表) = 0;
当类中有了纯虚函数,这个类也被称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
示例:
// 纯虚函数和抽象类
class Base
{
public:
// 纯虚函数:本类只要有一个纯虚函数,此类就是抽象类,无法实例化
virtual void func() = 0;
};
class Son :public Base
{
public:
// 对父类中纯虚函数进行重写:否则无法实例化对象 virtual 关键字可以不写
void func()
{
cout << "哈哈哈哈" << endl;
}
};
void test01()
{
Son s;
s.func();
}
int main()
{
test01();
return 0;
}
4.7.4 多态案例二
案例:煮茶和煮咖啡
示例:
class AbstractDrinking
{
public:
// 煮水
virtual void boil() = 0;
// 冲泡
virtual void brew() = 0;
// 倒入杯中
virtual void pourInCup() = 0;
// 加入辅料
virtual void putSomething() = 0;
// 制作饮品
void makeDrink()
{
boil();
brew();
pourInCup();
putSomething();
}
};
// 制作咖啡
class Coffee : public AbstractDrinking
{
// 煮水
virtual void boil()
{
cout << "煮水" << endl;
}
// 冲泡
virtual void brew()
{
cout << "冲泡咖啡" << endl;
}
// 倒入杯中
virtual void pourInCup()
{
cout << "倒入杯中" << endl;
}
// 加入辅料
virtual void putSomething()
{
cout << "倒入糖" << endl;
}
};
// 泡茶
class Tea : public AbstractDrinking
{
// 煮水
virtual void boil()
{
cout << "煮水" << endl;
}
// 冲泡
virtual void brew()
{
cout << "冲泡咖啡" << endl;
}
// 倒入杯中
virtual void pourInCup()
{
cout << "倒入杯中" << endl;
}
// 加入辅料
virtual void putSomething()
{
cout << "倒入枸杞" << endl;
}
};
// 制作函数
void doWork(AbstractDrinking* abs)
{
abs->makeDrink();
delete abs; // 释放
}
void test01()
{
cout << "泡咖啡****************" << endl;
// 制作咖啡
doWork(new Coffee);
cout << endl << "泡茶****************" << endl;
// 泡茶
doWork(new Tea);
}
int main()
{
test01();
return 0;
}
运行结果
4.7.5 虚析构和纯虚构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯析构区别
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名(){}
纯虚析构语法:virtual ~类名() = 0
示例:
#include<iostream>
using namespace std;
#include <string>
// 虚析构和纯虚析构
class Animal
{
public:
Animal()
{
cout << "Animal 构造函数调用" << endl;
}
// 利用虚析构解决子类对象释放不干净的问题
/*virtual ~Animal()
{
cout << "Animal 析构函数调用" << endl;
}*/
// 纯虚析构 也需要代码实现,但并不是在子类中实现
// 有纯虚析构,则是抽象类,不可实例化
virtual ~Animal() = 0;
// 纯虚函数
virtual void speak() = 0;
};
// Animal 中纯虚析构实现
Animal::~Animal()
{
cout << "Animal中纯虚析构函数调用" << endl;
}
class Cat : public Animal
{
public:
string *name;
public:
Cat(string name)
{
cout << "Cat的构造函数调用" << endl;
this->name = new string(name);
}
~Cat()
{
if (name != NULL)
{
cout << "Cat 析构函数调用" << endl;
delete name;
name = NULL;
}
}
virtual void speak()
{
cout << *this->name << "小猫在说话" << endl;
}
};
void test01()
{
Animal* animal = new Cat("Tom");
animal->speak();
// 父类指针在析构的时候,不会调用子类中析构函数,导致子类堆区数据有内存泄露情况
delete animal;
}
int main()
{
test01();
return 0;
}
运行结果:
总结:
- 虚析构活纯虚析构就是用来解决通过父类指针释放子类对象的问题
- 如果子类中没有堆区数据,就可以不写虚析构或纯虚析构
- 拥有纯虚析构,此类为抽象类