C++学习记录4——继承和多态
一、继承
继承可减少代码重复,提高利用率
1.继承的基本语法
语法:class 子类:继承方式 父类
子类也称派生类,父类也称基类
派生类中的成员,一部分从基类继承(共性),一部分自己新增(个性)
class Basepage
{
public:
void header()
{
cout << "header" << endl;
}
void footer()
{
cout << "footer" << endl;
}
void left()
{
cout << "left" << endl;
}
};
class A :public Basepage //继承方式为public
{
public:
void test1()
{
cout << "1" << endl;
}
};
class B :public Basepage
{
public:
void test2()
{
cout << "2" << endl;
}
};
int main()
{
A a;
a.footer();
a.test1();
B b;
b.header();
b.test2();
return 0;
}
2.继承方式
- 公共继承:父类的公共、保护对应在子类中仍为公共、保护权限
- 保护继承:父类的公共、保护在子类中都为保护权限
- 私有继承:父类的公共、保护在子类中都变为私有权限
父类中的私有属性,子类无法访问
3.继承中的对象模型
子类继承时,会将父类的全部的非静态成员属性都继承下来保留,父类中的私有属性被编译器隐藏,子类无法访问,但确实被继承了。
class Basepage
{
public:
int m_a;
protected:
int m_b;
private:
int m_c;
};
class A :public Basepage
{
public:
int m_d;
};
int main()
{
A a;
return 0;
}
验证:利用VS自带的开发人员命令提示符工具(在开始菜单中)
打开
打开文件所在的文件夹,复制文件路径,若文件不在默认的C盘,需跳转盘符(如输入F:
跳转到F盘)
输入cd 文件路径
,回车,跳转文件路径
输入dir,查看当前目录下的内容
查看命名cl /d1 reportSingleClassLayout类名 文件名
报告单个类的布局(reportSingleClassLayout)
如下图可看到类的占用空间大小和继承父类的、自身创建的属性情况
4.继承中的构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
先构造父类,再构造子类,先析构子类,再析构父类
5.继承中同名成员的处理方式
对于继承的同名成员
访问子类中的同名成员,直接访问即可
访问父类中的同名成员,要加作用域
class Basepage
{
public:
int m_a=10;
};
class A :public Basepage
{
public:
int m_a=100; //与父类对象同名
};
int main()
{
A a;
cout<<"A m_a"<<a.m_a<<endl; //直接调用子类
cout<<"Basepage m_a"<<a.Basepage::m_a<<endl; //此处加作用域
return 0;
}
如果子类中出现和父类同名的成员函数,子类的成员会隐藏掉父类所有的同名成员函数(包括重载的函数),想访问父类中被隐藏的同名成员函数,都要加作用域
class Basepage
{
public:
void fun(){}
void fun(int a){} //重载的成员函数
};
class A :public Basepage
{
public:
void fun(){} //与父类对象同名
};
int main()
{
A a;
a.fun(); //默认调用子类函数
a.fun(10); //错误
a.Basepage::fun(10); //调用重载的父类函数
return 0;
}
6.继承同名静态成员的处理方式
与上相同,注意两种访问方式
访问子类中的同名成员,直接访问即可
访问父类中的同名成员,要加作用域
通过对象访问时完全相同
要注意静态成员可通过类名访问
A::m_a //通过子类名访问子类中m_a
Basepage::m_a //通过父类名访问父类中m_a
A::Basepage::m_a //通过子类名访问父类作用域下父类中m_a
7.多继承语法
C++中一个类可继承多个类
class 子类:继承方式 父类1,继承方式 父类2
多继承可能引发父类中有同名函数出现,需要加作用域区分
实际开发中不建议使用多继承
8.菱形继承(钻石继承)
概念:
两个子类继承同一个父类,又有某个类同时继承了上述两个子类,这种继承称为菱形继承或钻石继承
PS:类比马、驴和骡子似乎更适合……
- 两父类拥有相同数据,可以通过加作用域区分,但有两个数据,资源浪费且无意义
- 利用虚继承(继承之前加关键字virtual),则Animal类(最大)称为虚基类,可解决菱形继承问题
class Animal{};
class Sheep:virtual public Animal{};
class Tuo:virtual public Animal{};
class SheepTuo:public Sheep, public Tuo{};
虚基类指针(vbptr)指向虚基类表(vbtable),继承虚基类指针,通过虚基类表中的偏移量找到数据,数据只保留一份
二、多态
1.多态基本概念
多态分两类,多态一般指动态多态
- 静态多态:函数重载和运算符重载,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定——编译阶段确定函数地址
- 动态多态的函数地址晚绑定——运行阶段确定函数地址
编译阶段确定函数地址
class Animal
{
public:
void speak();
};
class Cat:public Animal
{
public:
void speak();
};
void dospeak(Animal &animal) //允许Animal &animal = cat
{
animal.speak(); //地址早绑定,会执行Animal中的speak()
}
int main()
{
Cat cat;
dospeak(cat); //C++允许父子类之间类型自动转换
}
运行阶段确定函数地址
此时的speak()调用由传入的对象类型确定
class Animal
{
public:
//此处加virtual称为虚函数
virtual void speak(); //可实现地址晚绑定
};
class Cat:public Animal
{
public:
//此处重写speak()函数
void speak(); //此句前virtual可加可不加
};
void dospeak(Animal &animal) //用父类指针或引用执行子类对象
{
animal.speak();
}
int main()
{
Cat cat; //传入sheep1对象
dospeak(cat); //就执行Sheep中的speak()
}
动态多态要满足的条件:
- 有继承关系
- 子类重写父类的虚函数(重写是对完全相同的函数写入不同内容)
动态多态的使用:
用父类指针或引用指向子类对象
2.多态的底层原理
此时同空类,占1个字节空间
class Animal
{
public:
void speak();
};
加virtual后占4个字节
class Animal
{
public:
virtual void speak();
};
占4个字节的其实是一个指针:
虚函数(表)指针(vfptr),指向虚函数表(vftable),表内记录虚函数地址
子类重写虚函数之前:
子类重写虚函数之后:
3.多态的优点与实例
多态的优点
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展和维护
实例:计算器
普通实现计算器,代码量相对较少,但想扩展新的功能要修改源码,在真实开发中提倡开闭原则:对扩展进行开放,对修改进行关闭。
//普通实现计算器操作
class Calculator
{
public:
int getResult(string oper)
{
if (oper == "+")
{
return m_num1 + m_num2;
}
else if (oper == "-")
{
return m_num1 - m_num2;
}
else if (oper == "*")
{
return m_num1 * m_num2;
}//想扩展新的功能要修改源码
//在真实开发中提倡开闭原则:对扩展进行开放,对修改进行关闭
}
int m_num1;
int m_num2;
};
void test02()
{
//创建计算器对象
Calculator c;
c.m_num1 = 10;
c.m_num2 = 10;
cout << c.m_num1 << "+" << c.m_num2 << "=" << c.getResult("+") << endl;
cout << c.m_num1 << "-" << c.m_num2 << "=" << c.getResult("-") << endl;
cout << c.m_num1 << "*" << c.m_num2 << "=" << c.getResult("*") << endl;
}
int main()
{
test02();
return 0;
}
多态实现计算器,代码量相对大,但体现多态的优点,提倡用多态设计程序架构。
//利用多态实现计算器操作
//实现计算器的抽象类
class AbstractCalculator
{
public:
virtual int getResult()
{
return 0;
}
int m_Num1;
int m_Num2;
};
//加法类
class Add : public AbstractCalculator
{
public:
virtual int getResult()
{
return m_Num1 + m_Num2;
}
};
//减法类
class Sub : public AbstractCalculator
{
public:
virtual int getResult()
{
return m_Num1 - m_Num2;
}
};
//乘法类
class Mul : public AbstractCalculator
{
public:
virtual int getResult()
{
return m_Num1 * m_Num2;
}
};
//扩展只需继续向下添加即可
void test03()//此处三类代码类似,可另写成函数,传入参数(AbstractCalculator* abc,string fun) ,减少代码量
{
//加法
AbstractCalculator* abc = new Add;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << "+" << abc->m_Num2 << "=" << abc->getResult() << endl;
//用完要释放
delete abc;
//减法
abc = new Sub;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << "-" << abc->m_Num2 << "=" << abc->getResult() << endl;
delete abc;
//乘法
abc = new Mul;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << "*" << abc->m_Num2 << "=" << abc->getResult() << endl;
delete abc;
}
int main()
{
test03();
return 0;
}
4.纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可将虚函数改为纯虚函数
语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
目的:倒逼子类重写纯虚函数实现多态
5.虚析构和纯虚析构
多态使用时,父类指针在析构时,不会调用子类的析构函数。若子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
解决方式:将父类中的析构函数改为虚析构或纯虚析构
虚析构和纯虚析构的共性
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构的区别
- 如果是纯虚析构,该类就属于抽象类,无法实例化对象
虚析构语法:virtual ~类名(){}
(析构函数前加virtual)
纯虚析构语法:
类内声明:virtual ~类名() = 0;
类外实现析构函数:类名::~类名(){析构代码}
(第一个类名表明作用域,第二个类名即为析构函数名)
注意:此处的纯虚析构既需要在父类中声明,也需要有具体的实现,因为父类中也可能有属性开辟堆区,需要执行析构代码释放