继承
c++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。
一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。 派生类中的成员,包含两大部分: 一类是从基类继承过来的,一类是自己增加的成员。 从基类继承过过来的表现其共性,而新增的成员体现了其个性。
继承的形式:
class 父类{};
class 子类:继承方式 父类名
{
//新增子类数据
};
继承方式:private protected public(推荐)
所有父类私有在子类中不可访问,
- 公共继承,父类 的保护属性成员在子类中还是保护属性,公共属性成员还是公共属性
- 保护继承,父类的保护属性成员,公共属性成员在子类中都是保护属性
- 私有继承,父类的保护属性成员,公共属性成员在子类中都是私有属性
#include <iostream>
using namespace std;
class fat
{
private:
int a;
protected:
int b;
public:
int c;
};
class son:public fat
{
public:
void fun(){
//cout<<a<<endl;a在父类是私有属性不可访问
//a=10;
b=12;
cout<<b<<endl;
cout<<c<<endl;
}
};
int main(int argc, char *argv[])
{
son caicai;
caicai.c=100;
//caicai.a=10;a在父类是私有属性不可访问
//caicai.b=1;b是保护属性
caicai.fun();
}
子类的构造析构顺序
#include <iostream>
using namespace std;
class fat
{
public:
int c;
fat()
{cout<<"fat的构造函数"<<endl;}
~fat()
{cout<<"fat的析构函数"<<endl;}
};
class son:public fat
{
public:
son()
{cout<<"son的构造函数"<<endl;}
~son()
{cout<<"son的析构函数"<<endl;}
};
int main(int argc, char *argv[])
{
son caicai;
}
fat的构造函数
son的构造函数
son的析构函数
fat的析构函数
构造是从父类向下构造,析构是子类向上依次构造,注意复合语句
子类调用成员对象、父类的有参构造
子类实例化对象时 会自动调用 成员对象、父类的默认构造。
子类实例对象时 必须使用初始化列表 调用成员对象、父类的有参构造。
初始化列表时:父类写类名称 成员对象用对象名
#include <iostream>
using namespace std;
class gra
{
public:
int a;
gra()
{cout<<"gra的构造函数"<<endl;}
gra(int &a)
{this->a=a;cout<<"gra的构造函数"<<endl;}
~gra()
{cout<<"gra的析构函数"<<endl;}
};
class fat
{
public:
int b;
fat()
{cout<<"fat的构造函数"<<endl;}
fat(int &b)
{this->b=b;cout<<"fat的构造函数"<<endl;}
~fat()
{cout<<"fat的析构函数"<<endl;}
};
class son:public fat //父类是fat
{
public:
gra ob; //包含了成员对象
int c;
son()
{cout<<"son的构造函数"<<endl;}
son(int a,int b,int c):ob(a),fat(b) //调用有参构造必须初始化列表,父类写类名称 成员对象用对象名
{ this->c=c;
cout<<"son的构造函数"<<endl;
cout<<a<<" "<<b<<" "<<c<<endl;
}
~son()
{cout<<"son的析构函数"<<endl;}
};
int main(int argc, char *argv[])
{
son caicai(23,45,67);
}
子类和父类的同名处理
同名成员 最简单 最安全的处理方式:加作用域
不加作用域
子类默认优先访问 子类的同名成员
必须加父类作用域 访问父类的同名成员
#include <iostream>
using namespace std;
class fat
{
public:
int c;
fat()
{cout<<"fat的构造函数"<<endl;}
fat(int a)
{ c=a;
cout<<"fat的有参函数"<<endl;}
~fat()
{cout<<"fat的析构函数"<<endl;}
};
class son:public fat
{
public:
int c;
son()
{cout<<"son的构造函数"<<endl;}
son(int a,int b):fat(a)
{ c=b;
cout<<"son的有参构造"<<endl;}
~son()
{cout<<"son的析构函数"<<endl;}
};
int main(int argc, char *argv[])
{
son caicai(12,34);
cout<<caicai.c<<endl; //子类默认优先访问 子类的同名成员
cout<<caicai.fat::c<<endl;//必须加父类作用域 访问父类的同名成员
}
子类和父类 同名成员函数
#include <iostream>
using namespace std;
class fat
{
public:
void fun(){
cout<<"hello im fat"<<endl;
}
};
class son:public fat
{
public:
void fun(){
cout<<"hello im son"<<endl;
}
};
int main(int argc, char *argv[])
{
son caicai;
caicai.fun();//子类默认优先访问 子类的同名函数
caicai.fat::fun();//必须加父类作用域 访问父类的同名函数
}
子类 重定义 父类的同名函数
重载:无继承,同一作用域,参数的个数不同、顺序不同、类型不同 都可重载
重定义:有继承, 子类 重定义 父类的同名函数(参数可以不同)(非虚函数)子类一旦 重定义了父类的同名函数(不管参数是否一致),子类中都将屏蔽父类所有的同名函数。
#include <iostream>
using namespace std;
class fat
{
public:
void fun(){
cout<<"hello im fat"<<endl;
}
void fun(int a){
cout<<a<<"hello im fat"<<endl;
}
void fun(int a,int b){
cout<<a*b<<"hello im fat"<<endl;
}
void fun2(int a,int b){
cout<<a*b<<"hello im fat"<<endl;
}
};
class son:public fat
{
public:
void fun(){
cout<<"hello im son"<<endl;
}
};
int main(int argc, char *argv[])
{
son caicai;
caicai.fun();//子类默认优先访问 子类的同名函数
//caicai.fun(10,20);不识别父类的同名函数
caicai.fun2(10,20);//不同名的函数可以识别
caicai.fat::fun(2,5);//必须加父类作用域 才能父类的同名函数
caicai.fat::fun();//必须加父类作用域 访问父类的同名函数
}
子类不能继承父类的成员
不是所有的函数都能自动从基类继承到派生类中。比如构造函数和析构函数,operator=也不能被继承。
多继承
我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。C++实际开发中不建议用多继承
格式:
class 父类1{};
class 父类2{};
class 子类:继承方式1 父类1, 继承方式2 父类2
{
//新增子类数据
};
多继承中的同名成员处理
如果多继承中 遇到同名成员 需要加父类作用域解决。
#include <iostream>
using namespace std;
class gra
{
public:
void fun(void)
{cout<<"gra"<<endl;}
};
class fat
{
public:
void fun(void)
{cout<<"fat"<<endl;}
};
class son:public fat,public gra //son有两个父类
{
public:
void fun(void)
{cout<<"son"<<endl;}
};
int main(int argc, char *argv[])
{
son caicai;
caicai.fun();//先访问 子类的同名函数
caicai.fat::fun();//加父类作用域 才能父类的同名函数
caicai.gra::fun();//加父类作用域 才能父类的同名函数
}
菱形继承
菱形继承:有公共祖先的继承 叫菱形继承。
最底层的子类 数据 会包含多份(公共祖先的数据)
#include <iostream>
using namespace std;
class gra
{public:int a;};
class fat:public gra{};
class son:public gra{};
class cai:public son,public fat{};
int main(int argc, char *argv[])
{
cai kunkun;
//kunkun.a=10;ambiguous,有歧义,因为两个父类中都有a
kunkun.fat::a=10;
cout<<kunkun.fat::a<<endl;
}
虚继承
虚继承 解决 菱形继承中 多份公共祖先数据的问题。
在继承方式 前加virtual修饰,子类虚继承父类 子类只会保存一份公共数据。
#include <iostream>
using namespace std;
class gra
{public:int a;};
class fat:virtual public gra{};//前加virtual修饰,子类虚继承父类 子类只会保存一份公共数据。
class son:virtual public gra{};//继承前加virtual关键字后,变为虚继承,此时公共的父类称为虚基类
class cai:public son,public fat{};
int main(int argc, char *argv[])
{
cai kunkun;
kunkun.a=10;
cout<<kunkun.a<<endl;
}
多态
多态是C++面向对象三大特性之一
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,重定义
- 动态多态: 虚函数
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
虚函数
父类指针(引用)保存 子类空间地址的目的 就是让算法通用(操作派生出的所有子类)。
父类指针 保存 子类空间地址有一个问题,父类指针调用成员函数实际没有调用子类的成员函数。
#include<iostream>
using namespace std;
class Gra
{
public:
void fun(void)
{cout<<"im Gra"<<endl;}
};
class Fat:public Gra
{
public:
void fun(void)
{cout<<"im fat"<<endl;}
};
int main()
{ Gra *caicai=new Fat;//父类指针指向子类对象
caicai->fun(); //但是父类指针调用函数调用的父类函数im Gra
return 0;
}
虚函数:成员函数前加virtual修饰,解决这一问题。
#include<iostream>
using namespace std;
class Gra
{
public:
virtual void fun(void)
{cout<<"im Gra"<<endl;}
};
class Fat:public Gra
{
public:
virtual void fun(void) //子类重写 父类的虚函数:函数名、返回值类型、参数类型个数顺序 必须完全一致
{cout<<"im fat"<<endl;}
};
class Mot:public Gra
{
public:
virtual void fun(void) //子类重写 父类的虚函数:函数名、返回值类型、参数类型个数顺序 必须完全一致
{cout<<"im mot"<<endl;}
};
int main()
{ Gra *caicai=new Fat;//父类指针指向子类对象
caicai->fun();//im fat,实际调用了子类的成员函数
Gra *kunkun=new Mot;
kunkun->fun();//im mot,实际调用了子类的成员函数
return 0;
}
多态条件:有继承、子类重写父类的虚函数,父类指针 指向子类空间。
虚函数的动态绑定机制
如果一个类中的成员函数 被virtual修饰,那么这个函数就是虚函数。类就会产生一个虚函数指针(vfptr)指向了一张虚函数表(vftable)。如果这个类 没有涉及到继承, 这时虚函数表中 纪录就是当前虚函数入口地址。
一旦子类重写父类虚地址,就会蒋子类的覆盖虚函数表中原来的入口地址。
重载、重定义、重写的区别
- 重载:同一作用域,同名函数,参数的顺序、个数、类型不同 都可以重载。函数的返回值类型不能作为,重载条件(函数重载、运算符重载)。
- 重定义:有继承,子类 重定义 父类的同名函数(非虚函数), 参数顺序、个数、类型可以不同。子类的同名函数会屏蔽父类的所有同名函数(可以通过作用域解决)。
- 重写(覆盖) : 有继承,子类 重写 父类的虚函数。返回值类型、函数名、参数顺序、个数、类型都必须一致。
纯虚函数
如果基类一定派生出子类,而子类一定会重写父类的虚函数,那么父类的虚函数中的函数体感觉是无意义,可不可以不写父类虚函数的函数体呢?可以的,那就必须了解纯虚函数。
#include<iostream>
using namespace std;
class Gra //有 纯虚函数的类 为抽象类
{
public:
virtual void fun(void)=0;//纯虚函数
};
class Fat:public Gra
{
public:
virtual void fun(void) //子类一定要重写 父类的所有纯虚函数
{cout<<"im fat"<<endl;}
};
class Mot:public Gra
{
public:
virtual void fun(void) //子类一定要重写 父类的所有纯虚函数
{cout<<"im mot"<<endl;}
};
int main()
{ //Gra pb;抽象类不能实例化对象
Gra *caicai=new Fat;//父类指针指向子类对象
caicai->fun();//im fat,实际调用了子类的函数
Gra *kunkun=new Mot;
kunkun->fun();//im mot,实际调用了子类的函数
return 0;
}
抽象类主要的目的 是设计 类的接口:
一个例子
#include<iostream>
using namespace std;
//抽象制作饮品
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 {
public:
//烧水
virtual void Boil() {
cout << "煮农夫山泉!" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡咖啡!" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "将咖啡倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加入牛奶!" << endl;
}
};
//制作茶水
class Tea : public AbstractDrinking {
public:
//烧水
virtual void Boil() {
cout << "煮自来水!" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡茶叶!" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "将茶水倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加入枸杞!" << endl;
}
};
//业务函数
void DoWork(AbstractDrinking* drink) {
drink->MakeDrink();
delete drink;
}
void test01() {
DoWork(new Coffee);
cout << "--------------" << endl;
DoWork(new Tea);
}
int main() {
test01();
system("pause");
return 0;
}
虚函数和纯虚函数的区别
- 虚函数:virtual修饰 有函数体 不会导致父类为抽象类。
- 纯虚函数:virtual修饰,=0,没有函数体 导致父类为抽象类。子类必须重写父类的所有纯虚函数。
虚析构函数
- virtual修饰析构函数
- 虚析构:通过父类指针 释放整个子类空间。
#include<iostream>
using namespace std;
class Gra //有 纯虚函数的类 为抽象类
{
public:
virtual void fun(void)=0;//纯虚函数
virtual ~Gra() //虚析构,不加virtual 只会调用父类的析构函数,不会调用子类的析构函数
{
cout<<"Gra的析构函数"<<endl;
}
};
class Fat:public Gra
{
public:
virtual void fun(void) //子类一定要重写 父类的所有纯虚函数
{cout<<"im fat"<<endl;}
~Fat()
{
cout<<"Fat的析构函数"<<endl;
}
};
class Mot:public Gra
{
public:
virtual void fun(void) //子类一定要重写 父类的所有纯虚函数
{cout<<"im mot"<<endl;}
~Mot()
{
cout<<"Mot的析构函数"<<endl;
}
};
int main()
{
Gra *caicai=new Fat;//父类指针指向子类对象
caicai->fun();
delete caicai; //释放全部空间,
return 0;
}
- 构造的顺序:父类—>成员---->子类
- 析构的顺序:子类—>成员---->父类
纯虚析构函数
纯虚析构的本质:是析构函数,各个类的回收工作。而且析构函数不能被继承。
必须为纯虚析构函数提供一个函数体。
纯虚析构函数 必须在类外实现.
#include<iostream>
using namespace std;
class Gra //有 纯虚函数的类 为抽象类
{
public:
virtual void fun(void)=0;//纯虚函数
virtual ~Gra()=0; //虚析构
};
Gra::~Gra(){ //函数体在类外,必须加作用域
cout<<"Gra的析构函数"<<endl;
}
class Fat:public Gra
{
public:
virtual void fun(void) //子类一定要重写 父类的所有纯虚函数
{cout<<"im fat"<<endl;}
~Fat()
{
cout<<"Fat的析构函数"<<endl;
}
};
class Mot:public Gra
{
public:
virtual void fun(void) //子类一定要重写 父类的所有纯虚函数
{cout<<"im mot"<<endl;}
~Mot()
{
cout<<"Mot的析构函数"<<endl;
}
};
int main()
{
Gra *caicai=new Fat;//父类指针指向子类对象
caicai->fun();
delete caicai;
return 0;
}
虚析构和纯虚析构的区别
- 虚析构:virtual修饰,有函数体,不会导致父类为抽象类。
- 纯虚析构:virtual修饰,=0,函数体必须类外实现,导致父类为抽象类。